Integrate host escalation with production sessions
This commit is contained in:
parent
254bb37764
commit
696dae610a
13 changed files with 1175 additions and 96 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1243,6 +1243,7 @@ dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"lazaret-core",
|
"lazaret-core",
|
||||||
"lazaret-linux",
|
"lazaret-linux",
|
||||||
|
"nix 0.31.3",
|
||||||
"rustix",
|
"rustix",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,11 @@ impl ResultSenderWindow {
|
||||||
pub const fn sent_count(&self) -> u64 {
|
pub const fn sent_count(&self) -> u64 {
|
||||||
self.sent_count
|
self.sent_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn acknowledged_count(&self) -> u64 {
|
||||||
|
self.acknowledged_count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -183,4 +188,9 @@ impl ResultCollector {
|
||||||
pub const fn highest_contiguous_sequence(&self) -> Option<u64> {
|
pub const fn highest_contiguous_sequence(&self) -> Option<u64> {
|
||||||
self.next_sequence.checked_sub(1)
|
self.next_sequence.checked_sub(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn received_count(&self) -> u64 {
|
||||||
|
self.next_sequence
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ ed25519-dalek.workspace = true
|
||||||
getrandom.workspace = true
|
getrandom.workspace = true
|
||||||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||||
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||||
|
nix.workspace = true
|
||||||
rustix.workspace = true
|
rustix.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ use lazaret_core::protocol::{
|
||||||
encode_signed, request_file_name, result_chunk_file_name, result_file_name,
|
encode_signed, request_file_name, result_chunk_file_name, result_file_name,
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
MailboxCancellation, MailboxRequest, MailboxRequestError, MailboxResult, MailboxResultAck,
|
EscalationOutcome, MailboxCancellation, MailboxRequest, MailboxRequestError, MailboxResult,
|
||||||
ProtocolVersion, ResultStream,
|
MailboxResultAck, ProtocolVersion, ResultStream,
|
||||||
};
|
};
|
||||||
use lazaret_linux::filesystem::RegularFileMailbox;
|
use lazaret_linux::filesystem::RegularFileMailbox;
|
||||||
|
|
||||||
|
|
@ -23,6 +23,11 @@ pub enum ClientError {
|
||||||
Window(ResultWindowError),
|
Window(ResultWindowError),
|
||||||
Random(getrandom::Error),
|
Random(getrandom::Error),
|
||||||
TerminalAlreadyReceived,
|
TerminalAlreadyReceived,
|
||||||
|
TerminalChunkCount {
|
||||||
|
received: u64,
|
||||||
|
committed: u64,
|
||||||
|
outcome: EscalationOutcome,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ClientError {
|
impl fmt::Display for ClientError {
|
||||||
|
|
@ -37,6 +42,14 @@ impl fmt::Display for ClientError {
|
||||||
Self::TerminalAlreadyReceived => {
|
Self::TerminalAlreadyReceived => {
|
||||||
formatter.write_str("escalation request already received a terminal result")
|
formatter.write_str("escalation request already received a terminal result")
|
||||||
}
|
}
|
||||||
|
Self::TerminalChunkCount {
|
||||||
|
received,
|
||||||
|
committed,
|
||||||
|
outcome,
|
||||||
|
} => write!(
|
||||||
|
formatter,
|
||||||
|
"terminal {outcome:?} commits {committed} chunks after {received} were received"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +63,7 @@ impl std::error::Error for ClientError {
|
||||||
Self::ProtocolCodec(error) => Some(error),
|
Self::ProtocolCodec(error) => Some(error),
|
||||||
Self::Window(error) => Some(error),
|
Self::Window(error) => Some(error),
|
||||||
Self::Random(error) => Some(error),
|
Self::Random(error) => Some(error),
|
||||||
Self::TerminalAlreadyReceived => None,
|
Self::TerminalAlreadyReceived | Self::TerminalChunkCount { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +141,7 @@ impl EscalationClient {
|
||||||
let signed = encode_signed(&request, &self.sandbox_key)?;
|
let signed = encode_signed(&request, &self.sandbox_key)?;
|
||||||
let (_, digest) = self.session.verify_request(&signed)?;
|
let (_, digest) = self.session.verify_request(&signed)?;
|
||||||
self.inbox
|
self.inbox
|
||||||
.publish(&request_file_name(request_id), &signed)?;
|
.publish_replacing(&request_file_name(request_id), &signed)?;
|
||||||
Ok(ClientRequest {
|
Ok(ClientRequest {
|
||||||
client: self,
|
client: self,
|
||||||
request_id,
|
request_id,
|
||||||
|
|
@ -178,7 +191,7 @@ impl ClientRequest<'_> {
|
||||||
let signed = encode_signed(&cancellation, &self.client.sandbox_key)?;
|
let signed = encode_signed(&cancellation, &self.client.sandbox_key)?;
|
||||||
self.client
|
self.client
|
||||||
.inbox
|
.inbox
|
||||||
.publish(&cancellation_file_name(self.request_id), &signed)?;
|
.publish_replacing(&cancellation_file_name(self.request_id), &signed)?;
|
||||||
self.cancellation_published = true;
|
self.cancellation_published = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +230,7 @@ impl ClientRequest<'_> {
|
||||||
highest_contiguous_sequence: chunk.sequence,
|
highest_contiguous_sequence: chunk.sequence,
|
||||||
};
|
};
|
||||||
let signed = encode_signed(&acknowledgment, &self.client.sandbox_key)?;
|
let signed = encode_signed(&acknowledgment, &self.client.sandbox_key)?;
|
||||||
self.client.inbox.publish(
|
self.client.inbox.publish_replacing(
|
||||||
&acknowledgment_file_name(self.request_id, chunk.sequence),
|
&acknowledgment_file_name(self.request_id, chunk.sequence),
|
||||||
&signed,
|
&signed,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -230,14 +243,21 @@ impl ClientRequest<'_> {
|
||||||
}
|
}
|
||||||
let result_name = result_file_name(self.request_id);
|
let result_name = result_file_name(self.request_id);
|
||||||
if self.client.outbox.names()?.contains(&result_name) {
|
if self.client.outbox.names()?.contains(&result_name) {
|
||||||
let encoded = self
|
let (encoded, _) = self
|
||||||
.client
|
.client
|
||||||
.outbox
|
.outbox
|
||||||
.consume(&result_name, MAX_SIGNED_RECORD_BYTES)?;
|
.read(&result_name, MAX_SIGNED_RECORD_BYTES)?;
|
||||||
let result =
|
let result =
|
||||||
self.client
|
self.client
|
||||||
.session
|
.session
|
||||||
.verify_result(&encoded, self.request_id, self.digest)?;
|
.verify_result(&encoded, self.request_id, self.digest)?;
|
||||||
|
if result.chunk_count != self.collector.received_count() {
|
||||||
|
return Err(ClientError::TerminalChunkCount {
|
||||||
|
received: self.collector.received_count(),
|
||||||
|
committed: result.chunk_count,
|
||||||
|
outcome: result.outcome,
|
||||||
|
});
|
||||||
|
}
|
||||||
self.collector.finish(&result)?;
|
self.collector.finish(&result)?;
|
||||||
self.terminal = true;
|
self.terminal = true;
|
||||||
return Ok(ClientEvent::Terminal(Box::new(result)));
|
return Ok(ClientEvent::Terminal(Box::new(result)));
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ use lazaret_core::records::{
|
||||||
};
|
};
|
||||||
use lazaret_escalation_client::{ClientEvent, EscalationClient};
|
use lazaret_escalation_client::{ClientEvent, EscalationClient};
|
||||||
use lazaret_linux::filesystem::RegularFileMailbox;
|
use lazaret_linux::filesystem::RegularFileMailbox;
|
||||||
|
use nix::sys::signal::{SigSet, SigmaskHow, Signal};
|
||||||
|
use nix::sys::signalfd::{SfdFlags, SignalFd};
|
||||||
|
|
||||||
struct Arguments {
|
struct Arguments {
|
||||||
inbox: PathBuf,
|
inbox: PathBuf,
|
||||||
|
|
@ -37,12 +39,23 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
||||||
let arguments = parse_arguments(std::env::args_os().skip(1).collect())?;
|
let arguments = parse_arguments_with_session(
|
||||||
let publication: ProtocolPublicationIdentity = read_record(&arguments.publication)?;
|
std::env::args_os().skip(1).collect(),
|
||||||
let bundle: SandboxCredentialBundle = read_record(&arguments.sandbox_credentials)?;
|
std::env::var_os("LAZARET_SESSION_ID"),
|
||||||
let registration: SessionRegistrationResponse = read_record(&arguments.registration)?;
|
)?;
|
||||||
let binding: SessionBinding = read_record(&arguments.session_binding)?;
|
let publication: ProtocolPublicationIdentity =
|
||||||
let sandbox_key_bytes: [u8; 32] = bundle.sandbox_private_key.as_slice().try_into()?;
|
read_record(&arguments.publication).map_err(|error| context("publication", error))?;
|
||||||
|
let bundle: SandboxCredentialBundle = read_record(&arguments.sandbox_credentials)
|
||||||
|
.map_err(|error| context("sandbox credentials", error))?;
|
||||||
|
let registration: SessionRegistrationResponse = read_record(&arguments.registration)
|
||||||
|
.map_err(|error| context("session registration", error))?;
|
||||||
|
let binding: SessionBinding = read_record(&arguments.session_binding)
|
||||||
|
.map_err(|error| context("session binding", error))?;
|
||||||
|
let sandbox_key_bytes: [u8; 32] = bundle
|
||||||
|
.sandbox_private_key
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|error| context("sandbox signing key", error))?;
|
||||||
let sandbox_key = SigningKey::from_bytes(&sandbox_key_bytes);
|
let sandbox_key = SigningKey::from_bytes(&sandbox_key_bytes);
|
||||||
let session = VerifiedSession::new(
|
let session = VerifiedSession::new(
|
||||||
publication.trust_manifest_sha256,
|
publication.trust_manifest_sha256,
|
||||||
|
|
@ -52,16 +65,29 @@ fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
||||||
bundle.coordinator_protocol_credential(),
|
bundle.coordinator_protocol_credential(),
|
||||||
bundle.sandbox_protocol_credential(),
|
bundle.sandbox_protocol_credential(),
|
||||||
registration.session_capability,
|
registration.session_capability,
|
||||||
)?;
|
)
|
||||||
|
.map_err(|error| context("session authority", error))?;
|
||||||
let client = EscalationClient::new(
|
let client = EscalationClient::new(
|
||||||
RegularFileMailbox::open(&arguments.inbox)?,
|
RegularFileMailbox::open(&arguments.inbox)
|
||||||
RegularFileMailbox::open(&arguments.outbox)?,
|
.map_err(|error| context("request mailbox", error))?,
|
||||||
|
RegularFileMailbox::open(&arguments.outbox)
|
||||||
|
.map_err(|error| context("result mailbox", error))?,
|
||||||
session,
|
session,
|
||||||
sandbox_key,
|
sandbox_key,
|
||||||
);
|
);
|
||||||
let mut request = client.request(arguments.command_argv, arguments.justification)?;
|
let mut cancellation =
|
||||||
|
CancellationSignals::register().map_err(|error| context("cancellation signals", error))?;
|
||||||
|
let mut request = client
|
||||||
|
.request(arguments.command_argv, arguments.justification)
|
||||||
|
.map_err(|error| context("request publication", error))?;
|
||||||
loop {
|
loop {
|
||||||
match request.poll()? {
|
if cancellation.requested()? {
|
||||||
|
request.cancel()?;
|
||||||
|
}
|
||||||
|
match request
|
||||||
|
.poll()
|
||||||
|
.map_err(|error| context("result collection", error))?
|
||||||
|
{
|
||||||
ClientEvent::Output { stream, bytes, .. } => match stream {
|
ClientEvent::Output { stream, bytes, .. } => match stream {
|
||||||
lazaret_core::records::ResultStream::Stdout => {
|
lazaret_core::records::ResultStream::Stdout => {
|
||||||
io::stdout().write_all(&bytes)?;
|
io::stdout().write_all(&bytes)?;
|
||||||
|
|
@ -89,6 +115,58 @@ fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn context(label: &str, error: impl std::fmt::Display) -> io::Error {
|
||||||
|
io::Error::other(format!("{label}: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CancellationSignals {
|
||||||
|
descriptor: SignalFd,
|
||||||
|
previous: SigSet,
|
||||||
|
published: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CancellationSignals {
|
||||||
|
fn register() -> io::Result<Self> {
|
||||||
|
let mut signals = SigSet::empty();
|
||||||
|
signals.add(Signal::SIGINT);
|
||||||
|
signals.add(Signal::SIGTERM);
|
||||||
|
let previous = signals
|
||||||
|
.thread_swap_mask(SigmaskHow::SIG_BLOCK)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
let descriptor =
|
||||||
|
SignalFd::with_flags(&signals, SfdFlags::SFD_CLOEXEC | SfdFlags::SFD_NONBLOCK)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
Ok(Self {
|
||||||
|
descriptor,
|
||||||
|
previous,
|
||||||
|
published: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requested(&mut self) -> io::Result<bool> {
|
||||||
|
if self.published {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.descriptor
|
||||||
|
.read_signal()
|
||||||
|
.map_err(io::Error::from)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
self.published = true;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for CancellationSignals {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.previous.thread_set_mask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_record<T>(path: &std::path::Path) -> Result<T, Box<dyn std::error::Error>>
|
fn read_record<T>(path: &std::path::Path) -> Result<T, Box<dyn std::error::Error>>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned + serde::Serialize,
|
T: serde::de::DeserializeOwned + serde::Serialize,
|
||||||
|
|
@ -117,7 +195,10 @@ where
|
||||||
Ok(decode(&bytes)?)
|
Ok(decode(&bytes)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_arguments(mut arguments: Vec<OsString>) -> Result<Arguments, io::Error> {
|
fn parse_arguments_with_session(
|
||||||
|
mut arguments: Vec<OsString>,
|
||||||
|
session: Option<OsString>,
|
||||||
|
) -> Result<Arguments, io::Error> {
|
||||||
let separator = arguments
|
let separator = arguments
|
||||||
.iter()
|
.iter()
|
||||||
.position(|argument| argument == "--")
|
.position(|argument| argument == "--")
|
||||||
|
|
@ -156,6 +237,28 @@ fn parse_arguments(mut arguments: Vec<OsString>) -> Result<Arguments, io::Error>
|
||||||
_ => return Err(io::Error::other("unknown client option")),
|
_ => return Err(io::Error::other("unknown client option")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(session) = session {
|
||||||
|
let session = session
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| io::Error::other("LAZARET_SESSION_ID is not UTF-8"))?;
|
||||||
|
if session.len() != 64
|
||||||
|
|| !session
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
||||||
|
{
|
||||||
|
return Err(io::Error::other("LAZARET_SESSION_ID is invalid"));
|
||||||
|
}
|
||||||
|
let credentials = PathBuf::from("/run/lazaret/credentials");
|
||||||
|
inbox.get_or_insert_with(|| PathBuf::from("/run/lazaret/mailbox/requests"));
|
||||||
|
outbox.get_or_insert_with(|| PathBuf::from("/run/lazaret/mailbox/results"));
|
||||||
|
publication.get_or_insert_with(|| credentials.join("publication.postcard"));
|
||||||
|
sandbox_credentials.get_or_insert_with(|| credentials.join("sandbox-credentials.postcard"));
|
||||||
|
registration.get_or_insert_with(|| {
|
||||||
|
credentials.join(format!("session-{session}-registration.postcard"))
|
||||||
|
});
|
||||||
|
session_binding
|
||||||
|
.get_or_insert_with(|| credentials.join(format!("session-{session}-binding.postcard")));
|
||||||
|
}
|
||||||
Ok(Arguments {
|
Ok(Arguments {
|
||||||
inbox: inbox.ok_or_else(|| io::Error::other("inbox path is missing"))?,
|
inbox: inbox.ok_or_else(|| io::Error::other("inbox path is missing"))?,
|
||||||
outbox: outbox.ok_or_else(|| io::Error::other("outbox path is missing"))?,
|
outbox: outbox.ok_or_else(|| io::Error::other("outbox path is missing"))?,
|
||||||
|
|
@ -173,7 +276,7 @@ fn parse_arguments(mut arguments: Vec<OsString>) -> Result<Arguments, io::Error>
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::parse_arguments;
|
use super::parse_arguments_with_session;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
fn valid_arguments() -> Vec<OsString> {
|
fn valid_arguments() -> Vec<OsString> {
|
||||||
|
|
@ -204,7 +307,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_preserves_exact_argv_without_shell_interpretation() {
|
fn parser_preserves_exact_argv_without_shell_interpretation() {
|
||||||
let parsed = parse_arguments(valid_arguments()).unwrap();
|
let parsed = parse_arguments_with_session(valid_arguments(), None).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.command_argv,
|
parsed.command_argv,
|
||||||
[
|
[
|
||||||
|
|
@ -219,14 +322,48 @@ mod tests {
|
||||||
fn parser_refuses_missing_separator_unknown_options_and_empty_argv() {
|
fn parser_refuses_missing_separator_unknown_options_and_empty_argv() {
|
||||||
let mut missing_separator = valid_arguments();
|
let mut missing_separator = valid_arguments();
|
||||||
missing_separator.retain(|argument| argument != "--");
|
missing_separator.retain(|argument| argument != "--");
|
||||||
assert!(parse_arguments(missing_separator).is_err());
|
assert!(parse_arguments_with_session(missing_separator, None).is_err());
|
||||||
|
|
||||||
let mut unknown = valid_arguments();
|
let mut unknown = valid_arguments();
|
||||||
unknown[0] = OsString::from("--unknown");
|
unknown[0] = OsString::from("--unknown");
|
||||||
assert!(parse_arguments(unknown).is_err());
|
assert!(parse_arguments_with_session(unknown, None).is_err());
|
||||||
|
|
||||||
let mut empty = valid_arguments();
|
let mut empty = valid_arguments();
|
||||||
empty.truncate(empty.iter().position(|argument| argument == "--").unwrap() + 1);
|
empty.truncate(empty.iter().position(|argument| argument == "--").unwrap() + 1);
|
||||||
assert!(parse_arguments(empty).is_err());
|
assert!(parse_arguments_with_session(empty, None).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_environment_supplies_only_fixed_generation_mount_paths() {
|
||||||
|
let arguments = ["--justification", "needed", "--", "/usr/bin/tool"]
|
||||||
|
.into_iter()
|
||||||
|
.map(OsString::from)
|
||||||
|
.collect();
|
||||||
|
let session = "11".repeat(32);
|
||||||
|
let parsed =
|
||||||
|
parse_arguments_with_session(arguments, Some(OsString::from(&session))).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
parsed.inbox,
|
||||||
|
std::path::Path::new("/run/lazaret/mailbox/requests")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.outbox,
|
||||||
|
std::path::Path::new("/run/lazaret/mailbox/results")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.registration,
|
||||||
|
std::path::Path::new(&format!(
|
||||||
|
"/run/lazaret/credentials/session-{session}-registration.postcard"
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
let arguments = ["--justification", "needed", "--", "/usr/bin/tool"]
|
||||||
|
.into_iter()
|
||||||
|
.map(OsString::from)
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
parse_arguments_with_session(arguments, Some(OsString::from("not-an-identity")))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use lazaret_core::records::{
|
||||||
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
|
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
|
||||||
ParticipantState, PreparedArtifact,
|
ParticipantState, PreparedArtifact,
|
||||||
};
|
};
|
||||||
use lazaret_linux::process::current_process_identity;
|
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
|
||||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||||
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||||
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||||
|
|
@ -25,6 +25,9 @@ use crate::control::{
|
||||||
TrustAnchor,
|
TrustAnchor,
|
||||||
};
|
};
|
||||||
use crate::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
use crate::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
||||||
|
use crate::escalation::{
|
||||||
|
ProductionEscalation, ProductionEscalationHandle, ProductionEscalationInputs,
|
||||||
|
};
|
||||||
use crate::git_routing::GitRoutingCoordinator;
|
use crate::git_routing::GitRoutingCoordinator;
|
||||||
use crate::lifecycle::{
|
use crate::lifecycle::{
|
||||||
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
|
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
|
||||||
|
|
@ -94,6 +97,8 @@ pub struct ProductionLifecycleAdapter<'a> {
|
||||||
trust_anchor: Option<TrustAnchor>,
|
trust_anchor: Option<TrustAnchor>,
|
||||||
staged_control: Option<StagedControlFace>,
|
staged_control: Option<StagedControlFace>,
|
||||||
published_control: Option<Arc<PublishedControlFace>>,
|
published_control: Option<Arc<PublishedControlFace>>,
|
||||||
|
escalation: Option<ProductionEscalation<'a>>,
|
||||||
|
escalation_handle: Option<ProductionEscalationHandle>,
|
||||||
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
||||||
runtime: Option<CommittedSandboxRuntime>,
|
runtime: Option<CommittedSandboxRuntime>,
|
||||||
state_anchor_mount: Option<MountIdentity>,
|
state_anchor_mount: Option<MountIdentity>,
|
||||||
|
|
@ -191,6 +196,8 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
trust_anchor: None,
|
trust_anchor: None,
|
||||||
staged_control: None,
|
staged_control: None,
|
||||||
published_control: None,
|
published_control: None,
|
||||||
|
escalation: None,
|
||||||
|
escalation_handle: None,
|
||||||
awaiting_runtime: None,
|
awaiting_runtime: None,
|
||||||
runtime: None,
|
runtime: None,
|
||||||
state_anchor_mount: None,
|
state_anchor_mount: None,
|
||||||
|
|
@ -207,6 +214,12 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
self.published_control.as_ref().map(Arc::clone)
|
self.published_control.as_ref().map(Arc::clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn take_escalation(
|
||||||
|
&mut self,
|
||||||
|
) -> Option<(ProductionEscalation<'a>, ProductionEscalationHandle)> {
|
||||||
|
Some((self.escalation.take()?, self.escalation_handle.take()?))
|
||||||
|
}
|
||||||
|
|
||||||
fn require_generation(&self, generation: &Generation) -> Result<(), ProductionLifecycleError> {
|
fn require_generation(&self, generation: &Generation) -> Result<(), ProductionLifecycleError> {
|
||||||
if generation.domain_id == self.plan.generation_identity.domain_id
|
if generation.domain_id == self.plan.generation_identity.domain_id
|
||||||
&& generation.generation_id == self.plan.generation_identity.generation_id
|
&& generation.generation_id == self.plan.generation_identity.generation_id
|
||||||
|
|
@ -382,10 +395,48 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn stage_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
let anchor = self
|
let mut anchor = self
|
||||||
.trust_anchor
|
.trust_anchor
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| ProductionLifecycleError("Trust Anchor is absent".into()))?;
|
.ok_or_else(|| ProductionLifecycleError("Trust Anchor is absent".into()))?;
|
||||||
|
if self
|
||||||
|
.plan
|
||||||
|
.configuration
|
||||||
|
.fingerprint_record()
|
||||||
|
.escalation_enabled
|
||||||
|
{
|
||||||
|
let publication = anchor.publication_identity();
|
||||||
|
let coordinator_key = anchor.coordinator_protocol_key();
|
||||||
|
let credentials = anchor.take_sandbox_credential_bundle().map_err(failure)?;
|
||||||
|
let (credential_files, inbox, outbox) = self
|
||||||
|
.plan
|
||||||
|
.runtime_staging
|
||||||
|
.escalation_mailboxes(self.plan.invoking_uid)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let ownership = ProcessOwnershipContext {
|
||||||
|
domain_id: self.plan.generation_identity.domain_id,
|
||||||
|
generation_id: self.plan.generation_identity.generation_id,
|
||||||
|
coordinator_id: self.plan.generation_identity.coordinator_id,
|
||||||
|
slot: lazaret_core::records::HostProcessOwnershipSlot::EscalationHostProcess(
|
||||||
|
lazaret_core::identity::RequestId::new([0; 32]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let (escalation, handle) = ProductionEscalation::new(ProductionEscalationInputs {
|
||||||
|
configuration: self.plan.configuration,
|
||||||
|
publication,
|
||||||
|
credentials,
|
||||||
|
credential_files,
|
||||||
|
inbox,
|
||||||
|
outbox,
|
||||||
|
prompt_transport: Arc::clone(&self.plan.prompt_transport),
|
||||||
|
coordinator_key,
|
||||||
|
ownership,
|
||||||
|
invoking_uid: self.plan.invoking_uid,
|
||||||
|
})
|
||||||
|
.map_err(failure)?;
|
||||||
|
self.escalation = Some(escalation);
|
||||||
|
self.escalation_handle = Some(handle);
|
||||||
|
}
|
||||||
let generation_id = self.plan.generation_identity.generation_id;
|
let generation_id = self.plan.generation_identity.generation_id;
|
||||||
self.plan
|
self.plan
|
||||||
.store
|
.store
|
||||||
|
|
@ -691,6 +742,15 @@ pub(crate) fn serve_production_control_connection(
|
||||||
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
|
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
|
||||||
face: &PublishedControlFace,
|
face: &PublishedControlFace,
|
||||||
connection: AcceptedControlConnection,
|
connection: AcceptedControlConnection,
|
||||||
|
) -> Result<(), ControlError> {
|
||||||
|
serve_production_control_connection_with_escalation(lifecycle, face, connection, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serve_production_control_connection_with_escalation(
|
||||||
|
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
|
||||||
|
face: &PublishedControlFace,
|
||||||
|
connection: AcceptedControlConnection,
|
||||||
|
escalation: Option<&ProductionEscalationHandle>,
|
||||||
) -> Result<(), ControlError> {
|
) -> Result<(), ControlError> {
|
||||||
face.serve_connection_coordinated(
|
face.serve_connection_coordinated(
|
||||||
connection,
|
connection,
|
||||||
|
|
@ -701,7 +761,20 @@ pub(crate) fn serve_production_control_connection(
|
||||||
.admit_session(admission)
|
.admit_session(admission)
|
||||||
.map_err(|error| ControlError::Lifecycle(error.to_string()))
|
.map_err(|error| ControlError::Lifecycle(error.to_string()))
|
||||||
},
|
},
|
||||||
|
|binding, registration| {
|
||||||
|
if let Some(escalation) = escalation {
|
||||||
|
escalation
|
||||||
|
.register(binding, registration)
|
||||||
|
.map_err(|error| ControlError::Lifecycle(error.to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|session_id| {
|
|session_id| {
|
||||||
|
if let Some(escalation) = escalation {
|
||||||
|
escalation
|
||||||
|
.disconnect(session_id)
|
||||||
|
.map_err(|error| ControlError::Lifecycle(error.to_string()))?;
|
||||||
|
}
|
||||||
let mut lifecycle = lifecycle.lock().map_err(|_| ControlError::LockPoisoned)?;
|
let mut lifecycle = lifecycle.lock().map_err(|_| ControlError::LockPoisoned)?;
|
||||||
lifecycle
|
lifecycle
|
||||||
.disconnect_session(session_id)
|
.disconnect_session(session_id)
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,12 @@ use lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
|
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
|
||||||
use rustix::fs::{Mode, OFlags, openat};
|
use rustix::fs::{Mode, OFlags, openat};
|
||||||
|
|
||||||
use crate::bootstrap::{ProductionHostCoordinatorLifecycle, serve_production_control_connection};
|
use crate::bootstrap::{
|
||||||
|
ProductionHostCoordinatorLifecycle, serve_production_control_connection_with_escalation,
|
||||||
|
};
|
||||||
use crate::control::ControlError;
|
use crate::control::ControlError;
|
||||||
use crate::domain_store::HostCoordinatorDomainStore;
|
use crate::domain_store::HostCoordinatorDomainStore;
|
||||||
|
use crate::escalation::{ProductionEscalation, ProductionEscalationHandle};
|
||||||
use crate::production::{BootstrapOwnerGuard, ProductionColdStart, ProductionColdStartError};
|
use crate::production::{BootstrapOwnerGuard, ProductionColdStart, ProductionColdStartError};
|
||||||
use crate::production_selector::{InstalledProductPaths, select_production_cold_start};
|
use crate::production_selector::{InstalledProductPaths, select_production_cold_start};
|
||||||
use crate::waiter::BootstrapWaiterFace;
|
use crate::waiter::BootstrapWaiterFace;
|
||||||
|
|
@ -163,15 +166,16 @@ fn serve_published(
|
||||||
bootstrap_owner: &BootstrapOwnerGuard,
|
bootstrap_owner: &BootstrapOwnerGuard,
|
||||||
bootstrap_owner_session_id: SessionId,
|
bootstrap_owner_session_id: SessionId,
|
||||||
) -> Result<(), ProductionColdStartError> {
|
) -> Result<(), ProductionColdStartError> {
|
||||||
lifecycle
|
let escalation = take_escalation(lifecycle)?;
|
||||||
.lock()
|
|
||||||
.map_err(|_| ProductionColdStartError::service("production lifecycle lock is poisoned"))?
|
|
||||||
.adapter()
|
|
||||||
.published_control()
|
|
||||||
.ok_or_else(|| ProductionColdStartError::service("control face is absent"))?
|
|
||||||
.set_nonblocking(true)
|
|
||||||
.map_err(ProductionColdStartError::service)?;
|
|
||||||
std::thread::scope(|scope| {
|
std::thread::scope(|scope| {
|
||||||
|
let escalation_enabled = escalation.is_some();
|
||||||
|
let (escalation, escalation_handle) = escalation.unzip();
|
||||||
|
let (escalation_completed, escalation_completions) = std::sync::mpsc::sync_channel(1);
|
||||||
|
if let Some(escalation) = escalation {
|
||||||
|
scope.spawn(move || {
|
||||||
|
let _ = escalation_completed.send(escalation.run());
|
||||||
|
});
|
||||||
|
}
|
||||||
let (completed, completions) =
|
let (completed, completions) =
|
||||||
std::sync::mpsc::channel::<(u64, Result<(), ControlError>)>();
|
std::sync::mpsc::channel::<(u64, Result<(), ControlError>)>();
|
||||||
let mut active: BTreeMap<u64, std::os::unix::net::UnixStream> = BTreeMap::new();
|
let mut active: BTreeMap<u64, std::os::unix::net::UnixStream> = BTreeMap::new();
|
||||||
|
|
@ -181,6 +185,7 @@ fn serve_published(
|
||||||
active.remove(&connection_id);
|
active.remove(&connection_id);
|
||||||
connection_completion(result, false)?;
|
connection_completion(result, false)?;
|
||||||
}
|
}
|
||||||
|
require_escalation_running(&escalation_completions, escalation_enabled)?;
|
||||||
let face = {
|
let face = {
|
||||||
let mut lifecycle = lifecycle.lock().map_err(|_| {
|
let mut lifecycle = lifecycle.lock().map_err(|_| {
|
||||||
ProductionColdStartError::service("production lifecycle lock is poisoned")
|
ProductionColdStartError::service("production lifecycle lock is poisoned")
|
||||||
|
|
@ -193,6 +198,13 @@ fn serve_published(
|
||||||
while !active.is_empty() {
|
while !active.is_empty() {
|
||||||
wait_for_connection_completion(&completions, &mut active, true)?;
|
wait_for_connection_completion(&completions, &mut active, true)?;
|
||||||
}
|
}
|
||||||
|
if let Some(escalation) = &escalation_handle {
|
||||||
|
escalation.stop();
|
||||||
|
escalation_completions
|
||||||
|
.recv()
|
||||||
|
.map_err(ProductionColdStartError::service)?
|
||||||
|
.map_err(ProductionColdStartError::service)?;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
lifecycle
|
lifecycle
|
||||||
|
|
@ -224,9 +236,14 @@ fn serve_published(
|
||||||
})?;
|
})?;
|
||||||
active.insert(connection_id, cancellation);
|
active.insert(connection_id, cancellation);
|
||||||
let completed = completed.clone();
|
let completed = completed.clone();
|
||||||
|
let escalation = escalation_handle.clone();
|
||||||
scope.spawn(move || {
|
scope.spawn(move || {
|
||||||
let result =
|
let result = serve_production_control_connection_with_escalation(
|
||||||
serve_production_control_connection(lifecycle, &face, connection);
|
lifecycle,
|
||||||
|
&face,
|
||||||
|
connection,
|
||||||
|
escalation.as_ref(),
|
||||||
|
);
|
||||||
let _ = completed.send((connection_id, result));
|
let _ = completed.send((connection_id, result));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -241,6 +258,44 @@ fn serve_published(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn take_escalation<'resources>(
|
||||||
|
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, 'resources>>,
|
||||||
|
) -> Result<
|
||||||
|
Option<(ProductionEscalation<'resources>, ProductionEscalationHandle)>,
|
||||||
|
ProductionColdStartError,
|
||||||
|
> {
|
||||||
|
let mut lifecycle = lifecycle
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| ProductionColdStartError::service("production lifecycle lock is poisoned"))?;
|
||||||
|
lifecycle
|
||||||
|
.adapter()
|
||||||
|
.published_control()
|
||||||
|
.ok_or_else(|| ProductionColdStartError::service("control face is absent"))?
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.map_err(ProductionColdStartError::service)?;
|
||||||
|
Ok(lifecycle.adapter_mut().take_escalation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_escalation_running(
|
||||||
|
completions: &std::sync::mpsc::Receiver<Result<(), crate::escalation::EscalationError>>,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<(), ProductionColdStartError> {
|
||||||
|
match completions.try_recv() {
|
||||||
|
Ok(result) => {
|
||||||
|
result.map_err(ProductionColdStartError::service)?;
|
||||||
|
Err(ProductionColdStartError::service(
|
||||||
|
"production escalation service stopped before teardown",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Disconnected) if enabled => Err(
|
||||||
|
ProductionColdStartError::service("production escalation service was abandoned"),
|
||||||
|
),
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Empty | std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn wait_for_connection_completion(
|
fn wait_for_connection_completion(
|
||||||
completions: &std::sync::mpsc::Receiver<(u64, Result<(), ControlError>)>,
|
completions: &std::sync::mpsc::Receiver<(u64, Result<(), ControlError>)>,
|
||||||
active: &mut BTreeMap<u64, std::os::unix::net::UnixStream>,
|
active: &mut BTreeMap<u64, std::os::unix::net::UnixStream>,
|
||||||
|
|
|
||||||
|
|
@ -988,7 +988,14 @@ impl NonInteractiveSessionExecutor for RunscNonInteractiveExecutor {
|
||||||
for argument in &self.global_arguments {
|
for argument in &self.global_arguments {
|
||||||
target = target.argument(argument.clone());
|
target = target.argument(argument.clone());
|
||||||
}
|
}
|
||||||
target = target.argument("exec").argument(self.container.clone());
|
target = target
|
||||||
|
.argument("exec")
|
||||||
|
.argument("-env")
|
||||||
|
.argument(format!(
|
||||||
|
"LAZARET_SESSION_ID={}",
|
||||||
|
hex_lower(request.session_id.as_bytes())
|
||||||
|
))
|
||||||
|
.argument(self.container.clone());
|
||||||
for argument in &request.argv {
|
for argument in &request.argv {
|
||||||
target = target.argument(OsString::from_vec(argument.clone()));
|
target = target.argument(OsString::from_vec(argument.clone()));
|
||||||
}
|
}
|
||||||
|
|
@ -1530,9 +1537,23 @@ impl PublishedControlFace {
|
||||||
where
|
where
|
||||||
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
||||||
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
||||||
|
{
|
||||||
|
self.serve_next_coordinated_with_registration(admit, |_, _| Ok(()), disconnect)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serve_next_coordinated_with_registration<Admit, Registered, Disconnect>(
|
||||||
|
&self,
|
||||||
|
admit: Admit,
|
||||||
|
registered: Registered,
|
||||||
|
disconnect: Disconnect,
|
||||||
|
) -> Result<(), ControlError>
|
||||||
|
where
|
||||||
|
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
||||||
|
Registered: FnMut(SessionBinding, SessionRegistrationResponse) -> Result<(), ControlError>,
|
||||||
|
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
||||||
{
|
{
|
||||||
let connection = self.accept_connection()?;
|
let connection = self.accept_connection()?;
|
||||||
self.serve_connection_coordinated(connection, admit, disconnect)
|
self.serve_connection_coordinated(connection, admit, registered, disconnect)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn accept_connection(&self) -> Result<AcceptedControlConnection, ControlError> {
|
pub(crate) fn accept_connection(&self) -> Result<AcceptedControlConnection, ControlError> {
|
||||||
|
|
@ -1540,14 +1561,16 @@ impl PublishedControlFace {
|
||||||
Ok(AcceptedControlConnection { socket })
|
Ok(AcceptedControlConnection { socket })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn serve_connection_coordinated<Admit, Disconnect>(
|
pub(crate) fn serve_connection_coordinated<Admit, Registered, Disconnect>(
|
||||||
&self,
|
&self,
|
||||||
connection: AcceptedControlConnection,
|
connection: AcceptedControlConnection,
|
||||||
mut admit: Admit,
|
mut admit: Admit,
|
||||||
|
mut registered: Registered,
|
||||||
mut disconnect: Disconnect,
|
mut disconnect: Disconnect,
|
||||||
) -> Result<(), ControlError>
|
) -> Result<(), ControlError>
|
||||||
where
|
where
|
||||||
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
||||||
|
Registered: FnMut(SessionBinding, SessionRegistrationResponse) -> Result<(), ControlError>,
|
||||||
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
||||||
{
|
{
|
||||||
let socket = connection.socket;
|
let socket = connection.socket;
|
||||||
|
|
@ -1571,9 +1594,13 @@ impl PublishedControlFace {
|
||||||
let mut stream = rustls::StreamOwned::new(connection, socket);
|
let mut stream = rustls::StreamOwned::new(connection, socket);
|
||||||
match peer_certificate {
|
match peer_certificate {
|
||||||
None => self.serve_enrollment(&mut stream),
|
None => self.serve_enrollment(&mut stream),
|
||||||
Some(certificates) => {
|
Some(certificates) => self.serve_registration(
|
||||||
self.serve_registration(&mut stream, &certificates, &mut admit, &mut disconnect)
|
&mut stream,
|
||||||
}
|
&certificates,
|
||||||
|
&mut admit,
|
||||||
|
&mut registered,
|
||||||
|
&mut disconnect,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1596,15 +1623,17 @@ impl PublishedControlFace {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serve_registration<Admit, Disconnect>(
|
fn serve_registration<Admit, Registered, Disconnect>(
|
||||||
&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>],
|
||||||
admit: &mut Admit,
|
admit: &mut Admit,
|
||||||
|
registered: &mut Registered,
|
||||||
disconnect: &mut Disconnect,
|
disconnect: &mut Disconnect,
|
||||||
) -> Result<(), ControlError>
|
) -> Result<(), ControlError>
|
||||||
where
|
where
|
||||||
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
Admit: FnMut(ControlSessionAdmission) -> Result<(), ControlError>,
|
||||||
|
Registered: FnMut(SessionBinding, SessionRegistrationResponse) -> Result<(), ControlError>,
|
||||||
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
Disconnect: FnMut(SessionId) -> Result<(), ControlError>,
|
||||||
{
|
{
|
||||||
let [leaf] = certificates else {
|
let [leaf] = certificates else {
|
||||||
|
|
@ -1622,18 +1651,18 @@ impl PublishedControlFace {
|
||||||
&self.anchor.coordinator_protocol_key,
|
&self.anchor.coordinator_protocol_key,
|
||||||
)?;
|
)?;
|
||||||
let session_id = request.session_binding.session_id;
|
let session_id = request.session_binding.session_id;
|
||||||
|
let binding = request.session_binding.clone();
|
||||||
admit(ControlSessionAdmission {
|
admit(ControlSessionAdmission {
|
||||||
request,
|
request,
|
||||||
credential,
|
credential,
|
||||||
})?;
|
})?;
|
||||||
let result = (|| {
|
let result = (|| {
|
||||||
write_frame(
|
let registration = SessionRegistrationResponse {
|
||||||
stream,
|
|
||||||
&ControlResponse::Registered(SessionRegistrationResponse {
|
|
||||||
protocol_version: ProtocolVersion::V1,
|
protocol_version: ProtocolVersion::V1,
|
||||||
session_capability: capability,
|
session_capability: capability,
|
||||||
}),
|
};
|
||||||
)?;
|
registered(binding, registration.clone())?;
|
||||||
|
write_frame(stream, &ControlResponse::Registered(registration))?;
|
||||||
let request: ControlRequest = match read_frame(stream, MAX_CONTROL_BYTES) {
|
let request: ControlRequest = match read_frame(stream, MAX_CONTROL_BYTES) {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(ControlError::Io(error))
|
Err(ControlError::Io(error))
|
||||||
|
|
@ -2156,10 +2185,18 @@ mod tests {
|
||||||
runner.observed.lock().unwrap().as_ref().unwrap(),
|
runner.observed.lock().unwrap().as_ref().unwrap(),
|
||||||
&(
|
&(
|
||||||
OsString::from("runsc"),
|
OsString::from("runsc"),
|
||||||
["--root=/runtime", "exec", "container-id", "printf", "hello"]
|
vec![
|
||||||
.into_iter()
|
OsString::from("--root=/runtime"),
|
||||||
.map(OsString::from)
|
OsString::from("exec"),
|
||||||
.collect()
|
OsString::from("-env"),
|
||||||
|
OsString::from(format!(
|
||||||
|
"LAZARET_SESSION_ID={}",
|
||||||
|
hex_lower(session_id.as_bytes())
|
||||||
|
)),
|
||||||
|
OsString::from("container-id"),
|
||||||
|
OsString::from("printf"),
|
||||||
|
OsString::from("hello"),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,23 @@ use std::io::{self, Read};
|
||||||
use std::os::fd::{AsFd, OwnedFd};
|
use std::os::fd::{AsFd, OwnedFd};
|
||||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::{Arc, mpsc};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||||
use lazaret_core::identity::{RequestDigest, RequestId, SessionId, StreamDigest};
|
use lazaret_core::identity::{RequestDigest, RequestId, SessionId, StreamDigest};
|
||||||
use lazaret_core::protocol::{
|
use lazaret_core::protocol::{
|
||||||
MAX_RESULT_CHUNK_BYTES, ProtocolCodecError, ProtocolValidationError, RequestReplayGuard,
|
MAX_RESULT_CHUNK_BYTES, MAX_SIGNED_RECORD_BYTES, ProtocolCodecError, ProtocolValidationError,
|
||||||
ResultSenderWindow, ResultWindowError, VerifiedSession, encode_signed, result_chunk_file_name,
|
RequestReplayGuard, ResultSenderWindow, ResultWindowError, VerifiedSession,
|
||||||
result_file_name,
|
acknowledgment_file_name, cancellation_file_name, encode_signed, is_ready_record,
|
||||||
|
mailbox_session_name, result_chunk_file_name, result_file_name,
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
DeveloperDecisionKind, EscalationOutcome, GenerationIdentity, HostProcessOwnershipSlot,
|
DeveloperDecisionKind, EscalationOutcome, GenerationIdentity, HostProcessOwnershipSlot,
|
||||||
MailboxRequest, MailboxResult, MailboxResultChunk, ProcessOwnershipCommitAck, ProtocolVersion,
|
MailboxRequest, MailboxResult, MailboxResultChunk, ProcessOwnershipCommitAck,
|
||||||
ResultStream, StagedProcessOwnership,
|
ProtocolPublicationIdentity, ProtocolVersion, ResultStream, SandboxCredentialBundle,
|
||||||
|
SessionBinding, SessionRegistrationResponse, StagedProcessOwnership,
|
||||||
};
|
};
|
||||||
use lazaret_linux::filesystem::{MailboxEntryIdentity, RegularFileMailbox};
|
use lazaret_linux::filesystem::{MailboxEntryIdentity, RegularFileMailbox};
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
|
|
@ -29,6 +32,8 @@ use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, fchmod, fstat, mkdirat, o
|
||||||
use rustix::process::Signal;
|
use rustix::process::Signal;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::terminal::{SessionPromptTransport, TerminalError};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ApprovalSource {
|
pub enum ApprovalSource {
|
||||||
AutoRule,
|
AutoRule,
|
||||||
|
|
@ -192,6 +197,7 @@ pub enum EscalationError {
|
||||||
Protocol(ProtocolValidationError),
|
Protocol(ProtocolValidationError),
|
||||||
ProtocolCodec(ProtocolCodecError),
|
ProtocolCodec(ProtocolCodecError),
|
||||||
Window(ResultWindowError),
|
Window(ResultWindowError),
|
||||||
|
Terminal(TerminalError),
|
||||||
DuplicateSessionRequest,
|
DuplicateSessionRequest,
|
||||||
UnknownRequest,
|
UnknownRequest,
|
||||||
InvalidPhase,
|
InvalidPhase,
|
||||||
|
|
@ -205,6 +211,7 @@ impl fmt::Display for EscalationError {
|
||||||
Self::Protocol(error) => error.fmt(formatter),
|
Self::Protocol(error) => error.fmt(formatter),
|
||||||
Self::ProtocolCodec(error) => error.fmt(formatter),
|
Self::ProtocolCodec(error) => error.fmt(formatter),
|
||||||
Self::Window(error) => error.fmt(formatter),
|
Self::Window(error) => error.fmt(formatter),
|
||||||
|
Self::Terminal(error) => error.fmt(formatter),
|
||||||
Self::DuplicateSessionRequest => {
|
Self::DuplicateSessionRequest => {
|
||||||
formatter.write_str("session already has an active escalation request")
|
formatter.write_str("session already has an active escalation request")
|
||||||
}
|
}
|
||||||
|
|
@ -224,6 +231,7 @@ impl std::error::Error for EscalationError {
|
||||||
Self::Protocol(error) => Some(error),
|
Self::Protocol(error) => Some(error),
|
||||||
Self::ProtocolCodec(error) => Some(error),
|
Self::ProtocolCodec(error) => Some(error),
|
||||||
Self::Window(error) => Some(error),
|
Self::Window(error) => Some(error),
|
||||||
|
Self::Terminal(error) => Some(error),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -253,6 +261,27 @@ impl From<ResultWindowError> for EscalationError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TerminalError> for EscalationError {
|
||||||
|
fn from(error: TerminalError) -> Self {
|
||||||
|
Self::Terminal(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EscalationError {
|
||||||
|
fn is_record_refusal(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Protocol(_)
|
||||||
|
| Self::ProtocolCodec(_)
|
||||||
|
| Self::Window(_)
|
||||||
|
| Self::DuplicateSessionRequest
|
||||||
|
| Self::UnknownRequest
|
||||||
|
| Self::InvalidPhase
|
||||||
|
| Self::InvalidApproval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct RequestAuthority {
|
struct RequestAuthority {
|
||||||
session: VerifiedSession,
|
session: VerifiedSession,
|
||||||
request: MailboxRequest,
|
request: MailboxRequest,
|
||||||
|
|
@ -263,6 +292,8 @@ struct RequestAuthority {
|
||||||
struct PublishedChunk {
|
struct PublishedChunk {
|
||||||
name: OsString,
|
name: OsString,
|
||||||
identity: MailboxEntryIdentity,
|
identity: MailboxEntryIdentity,
|
||||||
|
stream: ResultStream,
|
||||||
|
bytes: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Execution {
|
struct Execution {
|
||||||
|
|
@ -270,13 +301,20 @@ struct Execution {
|
||||||
process: CapturedProcess,
|
process: CapturedProcess,
|
||||||
window: ResultSenderWindow,
|
window: ResultSenderWindow,
|
||||||
chunks: BTreeMap<u64, PublishedChunk>,
|
chunks: BTreeMap<u64, PublishedChunk>,
|
||||||
stdout: Sha256,
|
acknowledged_stdout: Sha256,
|
||||||
stderr: Sha256,
|
acknowledged_stderr: Sha256,
|
||||||
stdout_eof: bool,
|
stdout_eof: bool,
|
||||||
stderr_eof: bool,
|
stderr_eof: bool,
|
||||||
acknowledgment_deadline: Option<Instant>,
|
acknowledgment_deadline: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TerminatedExecution {
|
||||||
|
authority: RequestAuthority,
|
||||||
|
chunk_count: u64,
|
||||||
|
stdout_sha256: StreamDigest,
|
||||||
|
stderr_sha256: StreamDigest,
|
||||||
|
}
|
||||||
|
|
||||||
enum ActiveRequest {
|
enum ActiveRequest {
|
||||||
AwaitingApproval(Box<RequestAuthority>),
|
AwaitingApproval(Box<RequestAuthority>),
|
||||||
Executing(Box<Execution>),
|
Executing(Box<Execution>),
|
||||||
|
|
@ -471,9 +509,24 @@ impl EscalationCoordinator {
|
||||||
if decision.approved_command_argv.is_empty() {
|
if decision.approved_command_argv.is_empty() {
|
||||||
return Err(EscalationError::InvalidApproval);
|
return Err(EscalationError::InvalidApproval);
|
||||||
}
|
}
|
||||||
let executable = resolver.resolve(&decision.approved_command_argv)?;
|
if let Ok(executable) =
|
||||||
|
resolver
|
||||||
|
.resolve(&decision.approved_command_argv)
|
||||||
|
.and_then(|executable| {
|
||||||
validate_executable(executable.as_fd(), self.invoking_uid)?;
|
validate_executable(executable.as_fd(), self.invoking_uid)?;
|
||||||
|
Ok(executable)
|
||||||
|
})
|
||||||
|
{
|
||||||
Some(executable)
|
Some(executable)
|
||||||
|
} else {
|
||||||
|
let Some(ActiveRequest::AwaitingApproval(authority)) =
|
||||||
|
self.active.remove(&request_id)
|
||||||
|
else {
|
||||||
|
return Err(EscalationError::InvalidPhase);
|
||||||
|
};
|
||||||
|
self.finish(&authority, EscalationOutcome::Failed, None)?;
|
||||||
|
return Ok(EscalationPhase::Delivered(EscalationOutcome::Failed));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some(ActiveRequest::AwaitingApproval(authority)) = self.active.remove(&request_id)
|
let Some(ActiveRequest::AwaitingApproval(authority)) = self.active.remove(&request_id)
|
||||||
|
|
@ -549,8 +602,8 @@ impl EscalationCoordinator {
|
||||||
process,
|
process,
|
||||||
window: ResultSenderWindow::default(),
|
window: ResultSenderWindow::default(),
|
||||||
chunks: BTreeMap::new(),
|
chunks: BTreeMap::new(),
|
||||||
stdout: Sha256::new(),
|
acknowledged_stdout: Sha256::new(),
|
||||||
stderr: Sha256::new(),
|
acknowledged_stderr: Sha256::new(),
|
||||||
stdout_eof: false,
|
stdout_eof: false,
|
||||||
stderr_eof: false,
|
stderr_eof: false,
|
||||||
acknowledgment_deadline: None,
|
acknowledgment_deadline: None,
|
||||||
|
|
@ -581,8 +634,8 @@ impl EscalationCoordinator {
|
||||||
};
|
};
|
||||||
let progress = self.read_available(&mut execution, now);
|
let progress = self.read_available(&mut execution, now);
|
||||||
if progress.is_err() {
|
if progress.is_err() {
|
||||||
let authority = self.terminate(*execution)?;
|
let terminated = self.terminate(*execution)?;
|
||||||
self.finish(&authority, EscalationOutcome::Failed, None)?;
|
self.finish_terminated(&terminated, EscalationOutcome::Failed)?;
|
||||||
return Ok(EscalationPhase::Delivered(EscalationOutcome::Failed));
|
return Ok(EscalationPhase::Delivered(EscalationOutcome::Failed));
|
||||||
}
|
}
|
||||||
if execution.process.process().has_exited()?
|
if execution.process.process().has_exited()?
|
||||||
|
|
@ -592,9 +645,9 @@ impl EscalationCoordinator {
|
||||||
{
|
{
|
||||||
let execution = *execution;
|
let execution = *execution;
|
||||||
let authority = execution.authority;
|
let authority = execution.authority;
|
||||||
let stdout = StreamDigest::new(execution.stdout.finalize().into());
|
let stdout = StreamDigest::new(execution.acknowledged_stdout.finalize().into());
|
||||||
let stderr = StreamDigest::new(execution.stderr.finalize().into());
|
let stderr = StreamDigest::new(execution.acknowledged_stderr.finalize().into());
|
||||||
let chunk_count = execution.window.sent_count();
|
let chunk_count = execution.window.acknowledged_count();
|
||||||
let status = child_broker().reap_status(execution.process.into_process())?;
|
let status = child_broker().reap_status(execution.process.into_process())?;
|
||||||
self.finish_with_streams(
|
self.finish_with_streams(
|
||||||
&authority,
|
&authority,
|
||||||
|
|
@ -656,16 +709,18 @@ impl EscalationCoordinator {
|
||||||
stream,
|
stream,
|
||||||
bytes,
|
bytes,
|
||||||
};
|
};
|
||||||
match stream {
|
|
||||||
ResultStream::Stdout => execution.stdout.update(&chunk.bytes),
|
|
||||||
ResultStream::Stderr => execution.stderr.update(&chunk.bytes),
|
|
||||||
}
|
|
||||||
let name = result_chunk_file_name(chunk.request_id, sequence);
|
let name = result_chunk_file_name(chunk.request_id, sequence);
|
||||||
let encoded = encode_signed(&chunk, &self.coordinator_key)?;
|
let encoded = encode_signed(&chunk, &self.coordinator_key)?;
|
||||||
let identity = self.outbox.publish(&name, &encoded)?;
|
let identity = self.outbox.publish(&name, &encoded)?;
|
||||||
execution
|
execution.chunks.insert(
|
||||||
.chunks
|
sequence,
|
||||||
.insert(sequence, PublishedChunk { name, identity });
|
PublishedChunk {
|
||||||
|
name,
|
||||||
|
identity,
|
||||||
|
stream,
|
||||||
|
bytes: chunk.bytes,
|
||||||
|
},
|
||||||
|
);
|
||||||
if execution.acknowledgment_deadline.is_none() {
|
if execution.acknowledgment_deadline.is_none() {
|
||||||
execution.acknowledgment_deadline =
|
execution.acknowledgment_deadline =
|
||||||
Some(now + self.acknowledgment_timeout);
|
Some(now + self.acknowledgment_timeout);
|
||||||
|
|
@ -712,7 +767,12 @@ impl EscalationCoordinator {
|
||||||
for sequence in acknowledged {
|
for sequence in acknowledged {
|
||||||
if let Some(chunk) = execution.chunks.get(&sequence) {
|
if let Some(chunk) = execution.chunks.get(&sequence) {
|
||||||
self.outbox.remove_if_matches(&chunk.name, chunk.identity)?;
|
self.outbox.remove_if_matches(&chunk.name, chunk.identity)?;
|
||||||
execution.chunks.remove(&sequence);
|
}
|
||||||
|
if let Some(chunk) = execution.chunks.remove(&sequence) {
|
||||||
|
match chunk.stream {
|
||||||
|
ResultStream::Stdout => execution.acknowledged_stdout.update(&chunk.bytes),
|
||||||
|
ResultStream::Stderr => execution.acknowledged_stderr.update(&chunk.bytes),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
execution.acknowledgment_deadline = if execution.window.outstanding() == 0 {
|
execution.acknowledgment_deadline = if execution.window.outstanding() == 0 {
|
||||||
|
|
@ -752,11 +812,15 @@ impl EscalationCoordinator {
|
||||||
.active
|
.active
|
||||||
.remove(&request_id)
|
.remove(&request_id)
|
||||||
.ok_or(EscalationError::UnknownRequest)?;
|
.ok_or(EscalationError::UnknownRequest)?;
|
||||||
let authority = match active {
|
match active {
|
||||||
ActiveRequest::AwaitingApproval(authority) => *authority,
|
ActiveRequest::AwaitingApproval(authority) => {
|
||||||
ActiveRequest::Executing(execution) => self.terminate(*execution)?,
|
|
||||||
};
|
|
||||||
self.finish(&authority, EscalationOutcome::Cancelled, None)?;
|
self.finish(&authority, EscalationOutcome::Cancelled, None)?;
|
||||||
|
}
|
||||||
|
ActiveRequest::Executing(execution) => {
|
||||||
|
let terminated = self.terminate(*execution)?;
|
||||||
|
self.finish_terminated(&terminated, EscalationOutcome::Cancelled)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(EscalationPhase::Delivered(EscalationOutcome::Cancelled))
|
Ok(EscalationPhase::Delivered(EscalationOutcome::Cancelled))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -789,12 +853,16 @@ impl EscalationCoordinator {
|
||||||
.active
|
.active
|
||||||
.remove(request_id)
|
.remove(request_id)
|
||||||
.ok_or(EscalationError::UnknownRequest)?;
|
.ok_or(EscalationError::UnknownRequest)?;
|
||||||
let authority = match active {
|
match active {
|
||||||
ActiveRequest::AwaitingApproval(authority) => *authority,
|
ActiveRequest::AwaitingApproval(authority) => {
|
||||||
ActiveRequest::Executing(execution) => self.terminate(*execution)?,
|
|
||||||
};
|
|
||||||
self.finish(&authority, EscalationOutcome::TimedOut, None)?;
|
self.finish(&authority, EscalationOutcome::TimedOut, None)?;
|
||||||
}
|
}
|
||||||
|
ActiveRequest::Executing(execution) => {
|
||||||
|
let terminated = self.terminate(*execution)?;
|
||||||
|
self.finish_terminated(&terminated, EscalationOutcome::TimedOut)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(expired)
|
Ok(expired)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,14 +897,37 @@ impl EscalationCoordinator {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terminate(&mut self, execution: Execution) -> Result<RequestAuthority, EscalationError> {
|
fn terminate(&mut self, execution: Execution) -> Result<TerminatedExecution, EscalationError> {
|
||||||
execution.process.process().send_signal(Signal::KILL)?;
|
execution.process.process().send_signal(Signal::KILL)?;
|
||||||
let authority = execution.authority;
|
let authority = execution.authority;
|
||||||
|
let chunk_count = execution.window.acknowledged_count();
|
||||||
|
let stdout_sha256 = StreamDigest::new(execution.acknowledged_stdout.finalize().into());
|
||||||
|
let stderr_sha256 = StreamDigest::new(execution.acknowledged_stderr.finalize().into());
|
||||||
child_broker().reap(execution.process.into_process())?;
|
child_broker().reap(execution.process.into_process())?;
|
||||||
for chunk in execution.chunks.into_values() {
|
for chunk in execution.chunks.into_values() {
|
||||||
self.outbox.remove_if_matches(&chunk.name, chunk.identity)?;
|
self.outbox.remove_if_matches(&chunk.name, chunk.identity)?;
|
||||||
}
|
}
|
||||||
Ok(authority)
|
Ok(TerminatedExecution {
|
||||||
|
authority,
|
||||||
|
chunk_count,
|
||||||
|
stdout_sha256,
|
||||||
|
stderr_sha256,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_terminated(
|
||||||
|
&mut self,
|
||||||
|
execution: &TerminatedExecution,
|
||||||
|
outcome: EscalationOutcome,
|
||||||
|
) -> Result<(), EscalationError> {
|
||||||
|
self.finish_with_streams(
|
||||||
|
&execution.authority,
|
||||||
|
outcome,
|
||||||
|
None,
|
||||||
|
execution.chunk_count,
|
||||||
|
execution.stdout_sha256,
|
||||||
|
execution.stderr_sha256,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(
|
fn finish(
|
||||||
|
|
@ -895,6 +986,435 @@ impl EscalationCoordinator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SessionAuthorityFiles {
|
||||||
|
binding_name: OsString,
|
||||||
|
binding_identity: MailboxEntryIdentity,
|
||||||
|
registration_name: OsString,
|
||||||
|
registration_identity: MailboxEntryIdentity,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProductionSessionAuthority {
|
||||||
|
session: VerifiedSession,
|
||||||
|
files: SessionAuthorityFiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegistrationCommand {
|
||||||
|
binding: SessionBinding,
|
||||||
|
registration: SessionRegistrationResponse,
|
||||||
|
completed: mpsc::SyncSender<Result<(), String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProductionEscalationCommand {
|
||||||
|
Register(Box<RegistrationCommand>),
|
||||||
|
Disconnect {
|
||||||
|
session_id: SessionId,
|
||||||
|
completed: mpsc::SyncSender<Result<(), String>>,
|
||||||
|
},
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ProductionEscalationHandle {
|
||||||
|
commands: mpsc::Sender<ProductionEscalationCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProductionEscalationHandle {
|
||||||
|
pub(crate) fn register(
|
||||||
|
&self,
|
||||||
|
binding: SessionBinding,
|
||||||
|
registration: SessionRegistrationResponse,
|
||||||
|
) -> Result<(), EscalationError> {
|
||||||
|
let (completed, result) = mpsc::sync_channel(1);
|
||||||
|
self.commands
|
||||||
|
.send(ProductionEscalationCommand::Register(Box::new(
|
||||||
|
RegistrationCommand {
|
||||||
|
binding,
|
||||||
|
registration,
|
||||||
|
completed,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
.map_err(|_| io::Error::other("production escalation service stopped"))?;
|
||||||
|
result
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| io::Error::other("production escalation registration was abandoned"))?
|
||||||
|
.map_err(|error| io::Error::other(error).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disconnect(&self, session_id: SessionId) -> Result<(), EscalationError> {
|
||||||
|
let (completed, result) = mpsc::sync_channel(1);
|
||||||
|
self.commands
|
||||||
|
.send(ProductionEscalationCommand::Disconnect {
|
||||||
|
session_id,
|
||||||
|
completed,
|
||||||
|
})
|
||||||
|
.map_err(|_| io::Error::other("production escalation service stopped"))?;
|
||||||
|
result
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| io::Error::other("production escalation disconnect was abandoned"))?
|
||||||
|
.map_err(|error| io::Error::other(error).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop(&self) {
|
||||||
|
let _ = self.commands.send(ProductionEscalationCommand::Stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ProductionEscalation<'a> {
|
||||||
|
configuration: &'a ResolvedDomainConfiguration,
|
||||||
|
publication: ProtocolPublicationIdentity,
|
||||||
|
credentials: SandboxCredentialBundle,
|
||||||
|
credential_files: RegularFileMailbox,
|
||||||
|
inbox: RegularFileMailbox,
|
||||||
|
prompt_transport: Arc<SessionPromptTransport>,
|
||||||
|
coordinator: EscalationCoordinator,
|
||||||
|
ownership: LocalExecutionOwnership,
|
||||||
|
sessions: BTreeMap<SessionId, ProductionSessionAuthority>,
|
||||||
|
active: BTreeMap<RequestId, SessionId>,
|
||||||
|
commands: mpsc::Receiver<ProductionEscalationCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ProductionEscalationInputs<'a> {
|
||||||
|
pub configuration: &'a ResolvedDomainConfiguration,
|
||||||
|
pub publication: ProtocolPublicationIdentity,
|
||||||
|
pub credentials: SandboxCredentialBundle,
|
||||||
|
pub credential_files: RegularFileMailbox,
|
||||||
|
pub inbox: RegularFileMailbox,
|
||||||
|
pub outbox: RegularFileMailbox,
|
||||||
|
pub prompt_transport: Arc<SessionPromptTransport>,
|
||||||
|
pub coordinator_key: SigningKey,
|
||||||
|
pub ownership: ProcessOwnershipContext,
|
||||||
|
pub invoking_uid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ProductionEscalation<'a> {
|
||||||
|
pub(crate) fn new(
|
||||||
|
inputs: ProductionEscalationInputs<'a>,
|
||||||
|
) -> Result<(Self, ProductionEscalationHandle), EscalationError> {
|
||||||
|
inputs.credential_files.publish(
|
||||||
|
OsStr::new("publication.postcard"),
|
||||||
|
&lazaret_core::codec::encode(&inputs.publication)
|
||||||
|
.map_err(|error| io::Error::other(error.to_string()))?,
|
||||||
|
)?;
|
||||||
|
inputs.credential_files.publish(
|
||||||
|
OsStr::new("sandbox-credentials.postcard"),
|
||||||
|
&lazaret_core::codec::encode(&inputs.credentials)
|
||||||
|
.map_err(|error| io::Error::other(error.to_string()))?,
|
||||||
|
)?;
|
||||||
|
let (commands, receiver) = mpsc::channel();
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
configuration: inputs.configuration,
|
||||||
|
publication: inputs.publication,
|
||||||
|
credentials: inputs.credentials,
|
||||||
|
credential_files: inputs.credential_files,
|
||||||
|
inbox: inputs.inbox,
|
||||||
|
prompt_transport: inputs.prompt_transport,
|
||||||
|
coordinator: EscalationCoordinator::new(
|
||||||
|
inputs.coordinator_key,
|
||||||
|
inputs.outbox,
|
||||||
|
inputs.ownership,
|
||||||
|
inputs.invoking_uid,
|
||||||
|
Duration::from_secs(30),
|
||||||
|
Duration::from_secs(2),
|
||||||
|
),
|
||||||
|
ownership: LocalExecutionOwnership::default(),
|
||||||
|
sessions: BTreeMap::new(),
|
||||||
|
active: BTreeMap::new(),
|
||||||
|
commands: receiver,
|
||||||
|
},
|
||||||
|
ProductionEscalationHandle { commands },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run(mut self) -> Result<(), EscalationError> {
|
||||||
|
loop {
|
||||||
|
while let Ok(command) = self.commands.try_recv() {
|
||||||
|
if self.apply_command(command)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.process_requests()?;
|
||||||
|
self.process_active()?;
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_command(
|
||||||
|
&mut self,
|
||||||
|
command: ProductionEscalationCommand,
|
||||||
|
) -> Result<bool, EscalationError> {
|
||||||
|
match command {
|
||||||
|
ProductionEscalationCommand::Register(command) => {
|
||||||
|
let RegistrationCommand {
|
||||||
|
binding,
|
||||||
|
registration,
|
||||||
|
completed,
|
||||||
|
} = *command;
|
||||||
|
let result = self
|
||||||
|
.register(&binding, ®istration)
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
|
let failed = result.is_err();
|
||||||
|
let _ = completed.send(result);
|
||||||
|
if failed {
|
||||||
|
return Err(io::Error::other("session escalation registration failed").into());
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
ProductionEscalationCommand::Disconnect {
|
||||||
|
session_id,
|
||||||
|
completed,
|
||||||
|
} => {
|
||||||
|
let result = self
|
||||||
|
.disconnect(session_id)
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
|
let failed = result.is_err();
|
||||||
|
let _ = completed.send(result);
|
||||||
|
if failed {
|
||||||
|
return Err(io::Error::other("session escalation cleanup failed").into());
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
ProductionEscalationCommand::Stop => {
|
||||||
|
let sessions = self.sessions.keys().copied().collect::<Vec<_>>();
|
||||||
|
for session_id in sessions {
|
||||||
|
self.disconnect(session_id)?;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register(
|
||||||
|
&mut self,
|
||||||
|
binding: &SessionBinding,
|
||||||
|
registration: &SessionRegistrationResponse,
|
||||||
|
) -> Result<(), EscalationError> {
|
||||||
|
let session_id = binding.session_id;
|
||||||
|
if self.sessions.contains_key(&session_id) {
|
||||||
|
return Err(EscalationError::DuplicateSessionRequest);
|
||||||
|
}
|
||||||
|
let session = VerifiedSession::new(
|
||||||
|
self.publication.trust_manifest_sha256,
|
||||||
|
self.credentials.trust_manifest.tls_ca_sha256,
|
||||||
|
&self.credentials.trust_manifest,
|
||||||
|
binding.clone(),
|
||||||
|
self.credentials.coordinator_protocol_credential(),
|
||||||
|
self.credentials.sandbox_protocol_credential(),
|
||||||
|
registration.session_capability.clone(),
|
||||||
|
)?;
|
||||||
|
let prefix = mailbox_session_name(session_id);
|
||||||
|
let mut binding_name = prefix.clone();
|
||||||
|
binding_name.push("-binding.postcard");
|
||||||
|
let mut registration_name = prefix;
|
||||||
|
registration_name.push("-registration.postcard");
|
||||||
|
let binding_identity = self.credential_files.publish(
|
||||||
|
&binding_name,
|
||||||
|
&lazaret_core::codec::encode(binding)
|
||||||
|
.map_err(|error| io::Error::other(error.to_string()))?,
|
||||||
|
)?;
|
||||||
|
let registration_identity = match self.credential_files.publish(
|
||||||
|
®istration_name,
|
||||||
|
&lazaret_core::codec::encode(registration)
|
||||||
|
.map_err(|error| io::Error::other(error.to_string()))?,
|
||||||
|
) {
|
||||||
|
Ok(identity) => identity,
|
||||||
|
Err(error) => {
|
||||||
|
self.credential_files
|
||||||
|
.remove_if_matches(&binding_name, binding_identity)?;
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.sessions.insert(
|
||||||
|
session_id,
|
||||||
|
ProductionSessionAuthority {
|
||||||
|
session,
|
||||||
|
files: SessionAuthorityFiles {
|
||||||
|
binding_name,
|
||||||
|
binding_identity,
|
||||||
|
registration_name,
|
||||||
|
registration_identity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&mut self, session_id: SessionId) -> Result<(), EscalationError> {
|
||||||
|
self.coordinator.session_lost(session_id)?;
|
||||||
|
self.active.retain(|_, active| *active != session_id);
|
||||||
|
let _ = self.prompt_transport.finish_escalation(session_id);
|
||||||
|
if let Some(authority) = self.sessions.remove(&session_id) {
|
||||||
|
self.credential_files.remove_if_matches(
|
||||||
|
&authority.files.binding_name,
|
||||||
|
authority.files.binding_identity,
|
||||||
|
)?;
|
||||||
|
self.credential_files.remove_if_matches(
|
||||||
|
&authority.files.registration_name,
|
||||||
|
authority.files.registration_identity,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_requests(&mut self) -> Result<(), EscalationError> {
|
||||||
|
for name in self.inbox.names()? {
|
||||||
|
if !is_ready_record(&name) || !name.as_bytes().starts_with(b"request-") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok((bytes, identity)) = self.inbox.read(&name, MAX_SIGNED_RECORD_BYTES) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let matched = self.sessions.values().find_map(|authority| {
|
||||||
|
authority
|
||||||
|
.session
|
||||||
|
.verify_request(&bytes)
|
||||||
|
.ok()
|
||||||
|
.map(|(request, _)| (authority.session.clone(), request))
|
||||||
|
});
|
||||||
|
self.inbox.remove_if_matches(&name, identity)?;
|
||||||
|
let Some((session, request)) = matched else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let session_id = session.session_binding().session_id;
|
||||||
|
let request_id = request.request_id();
|
||||||
|
let auto = self
|
||||||
|
.configuration
|
||||||
|
.auto_approval_rules()
|
||||||
|
.iter()
|
||||||
|
.find_map(|rule| {
|
||||||
|
rule.authorize(&request)
|
||||||
|
.ok()
|
||||||
|
.map(|executable| (executable, request.command_argv().to_vec()))
|
||||||
|
});
|
||||||
|
let phase = if let Some((executable, command_argv)) = auto {
|
||||||
|
self.coordinator.receive_auto_approved(
|
||||||
|
session,
|
||||||
|
&bytes,
|
||||||
|
executable,
|
||||||
|
command_argv,
|
||||||
|
&mut self.ownership,
|
||||||
|
Instant::now(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let phase = self.coordinator.receive_for_developer_approval(
|
||||||
|
session.clone(),
|
||||||
|
&bytes,
|
||||||
|
Instant::now(),
|
||||||
|
);
|
||||||
|
let Ok(phase) = phase else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Err(error) = self.prompt_transport.queue_signed_request(session, &bytes) {
|
||||||
|
self.coordinator.session_lost(session_id)?;
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
Ok(phase)
|
||||||
|
};
|
||||||
|
let phase = match phase {
|
||||||
|
Ok(phase) => phase,
|
||||||
|
Err(error) if error.is_record_refusal() => continue,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
self.record_phase(request_id, session_id, phase)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_active(&mut self) -> Result<(), EscalationError> {
|
||||||
|
let active = self
|
||||||
|
.active
|
||||||
|
.iter()
|
||||||
|
.map(|(id, session)| (*id, *session))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (request_id, session_id) in &active {
|
||||||
|
if let Some(decision) = self.prompt_transport.take_decision(*session_id)? {
|
||||||
|
let phase = self.coordinator.apply_developer_decision(
|
||||||
|
*request_id,
|
||||||
|
&decision.signed_decision,
|
||||||
|
&mut DirectDeveloperExecutableResolver::open()?,
|
||||||
|
&mut self.ownership,
|
||||||
|
)?;
|
||||||
|
self.record_phase(*request_id, *session_id, phase)?;
|
||||||
|
}
|
||||||
|
if !self.active.contains_key(request_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let inbox_names = self.inbox.names()?;
|
||||||
|
let prefix = acknowledgment_prefix(*request_id);
|
||||||
|
for name in &inbox_names {
|
||||||
|
if !name.as_bytes().starts_with(&prefix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(bytes) = self.inbox.consume(name, MAX_SIGNED_RECORD_BYTES) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match self
|
||||||
|
.coordinator
|
||||||
|
.acknowledge(*request_id, &bytes, Instant::now())
|
||||||
|
{
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) if error.is_record_refusal() => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cancellation = cancellation_file_name(*request_id);
|
||||||
|
if inbox_names.contains(&cancellation) {
|
||||||
|
let Ok(bytes) = self.inbox.consume(&cancellation, MAX_SIGNED_RECORD_BYTES) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match self.coordinator.cancel(*request_id, &bytes) {
|
||||||
|
Ok(phase) => self.record_phase(*request_id, *session_id, phase)?,
|
||||||
|
Err(error) if error.is_record_refusal() => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.active.contains_key(request_id) {
|
||||||
|
let phase = self.coordinator.progress(*request_id, Instant::now())?;
|
||||||
|
self.record_phase(*request_id, *session_id, phase)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for request_id in self.coordinator.expire(Instant::now())? {
|
||||||
|
if let Some(session_id) = self.active.get(&request_id).copied() {
|
||||||
|
self.record_phase(
|
||||||
|
request_id,
|
||||||
|
session_id,
|
||||||
|
EscalationPhase::Delivered(EscalationOutcome::TimedOut),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_phase(
|
||||||
|
&mut self,
|
||||||
|
request_id: RequestId,
|
||||||
|
session_id: SessionId,
|
||||||
|
phase: EscalationPhase,
|
||||||
|
) -> Result<(), EscalationError> {
|
||||||
|
match phase {
|
||||||
|
EscalationPhase::AwaitingApproval => {
|
||||||
|
self.active.insert(request_id, session_id);
|
||||||
|
}
|
||||||
|
EscalationPhase::Executing => {
|
||||||
|
self.active.insert(request_id, session_id);
|
||||||
|
self.prompt_transport.begin_host_execution(session_id)?;
|
||||||
|
}
|
||||||
|
EscalationPhase::Delivered(_) => {
|
||||||
|
self.active.remove(&request_id);
|
||||||
|
self.prompt_transport.finish_escalation(session_id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acknowledgment_prefix(request_id: RequestId) -> Vec<u8> {
|
||||||
|
let example = acknowledgment_file_name(request_id, 0);
|
||||||
|
let bytes = example.as_bytes();
|
||||||
|
bytes[..bytes.len() - 26].to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_executable(executable: impl AsFd, invoking_uid: u32) -> io::Result<()> {
|
fn validate_executable(executable: impl AsFd, invoking_uid: u32) -> io::Result<()> {
|
||||||
let stat = fstat(executable)?;
|
let stat = fstat(executable)?;
|
||||||
let file_type = FileType::from_raw_mode(stat.st_mode);
|
let file_type = FileType::from_raw_mode(stat.st_mode);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ffi::OsString;
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
@ -16,7 +16,7 @@ use lazaret_core::records::{
|
||||||
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
|
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
|
||||||
};
|
};
|
||||||
use lazaret_ignored_workspace::git_common_directory;
|
use lazaret_ignored_workspace::git_common_directory;
|
||||||
use lazaret_linux::filesystem::{mount_identity, namespace_identity};
|
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
||||||
};
|
};
|
||||||
|
|
@ -707,6 +707,31 @@ impl RuntimeStaging {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn escalation_mailboxes(
|
||||||
|
&self,
|
||||||
|
invoking_uid: u32,
|
||||||
|
) -> Result<
|
||||||
|
(RegularFileMailbox, RegularFileMailbox, RegularFileMailbox),
|
||||||
|
ProductionColdStartError,
|
||||||
|
> {
|
||||||
|
let credentials = RegularFileMailbox::openat_private(
|
||||||
|
&self.control,
|
||||||
|
OsStr::new("credentials"),
|
||||||
|
invoking_uid,
|
||||||
|
)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let mailbox =
|
||||||
|
RegularFileMailbox::openat_private(&self.control, OsStr::new("mailbox"), invoking_uid)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let requests =
|
||||||
|
RegularFileMailbox::openat_private(&mailbox, OsStr::new("requests"), invoking_uid)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let results =
|
||||||
|
RegularFileMailbox::openat_private(&mailbox, OsStr::new("results"), invoking_uid)
|
||||||
|
.map_err(failure)?;
|
||||||
|
Ok((credentials, requests, results))
|
||||||
|
}
|
||||||
|
|
||||||
fn create(
|
fn create(
|
||||||
runtime_domain: &ConfinedPathPlan,
|
runtime_domain: &ConfinedPathPlan,
|
||||||
container_id: ContainerId,
|
container_id: ContainerId,
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,56 @@ impl SessionPromptTransport {
|
||||||
.map_err(|_| TerminalError::PromptValidation)?
|
.map_err(|_| TerminalError::PromptValidation)?
|
||||||
.remove(&session_id))
|
.remove(&session_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn begin_host_execution(&self, session_id: SessionId) -> Result<(), TerminalError> {
|
||||||
|
self.transition_session(session_id, SessionPhase::HostExecuting)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_escalation(&self, session_id: SessionId) -> Result<(), TerminalError> {
|
||||||
|
self.pending
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| TerminalError::PromptValidation)?
|
||||||
|
.remove(&session_id);
|
||||||
|
self.routed
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| TerminalError::PromptValidation)?
|
||||||
|
.remove(&session_id);
|
||||||
|
let snapshot = self.store.snapshot()?;
|
||||||
|
let Some(session) = snapshot
|
||||||
|
.sessions
|
||||||
|
.iter()
|
||||||
|
.find(|session| session.session_id == session_id)
|
||||||
|
else {
|
||||||
|
return Err(TerminalError::UnknownSession);
|
||||||
|
};
|
||||||
|
match session.session_phase {
|
||||||
|
SessionPhase::Attached => return Ok(()),
|
||||||
|
SessionPhase::Prompting | SessionPhase::HostExecuting => {}
|
||||||
|
SessionPhase::New
|
||||||
|
| SessionPhase::Waiting
|
||||||
|
| SessionPhase::Registering
|
||||||
|
| SessionPhase::Closed
|
||||||
|
| SessionPhase::Failed => return Err(TerminalError::InactiveRoute),
|
||||||
|
}
|
||||||
|
self.transition_session(session_id, SessionPhase::Attached)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transition_session(
|
||||||
|
&self,
|
||||||
|
session_id: SessionId,
|
||||||
|
session_phase: SessionPhase,
|
||||||
|
) -> Result<(), TerminalError> {
|
||||||
|
let snapshot = self.store.snapshot()?;
|
||||||
|
let mut session = snapshot
|
||||||
|
.sessions
|
||||||
|
.into_iter()
|
||||||
|
.find(|session| session.session_id == session_id)
|
||||||
|
.ok_or(TerminalError::UnknownSession)?;
|
||||||
|
session.session_phase = session_phase;
|
||||||
|
self.store
|
||||||
|
.apply(&[DomainStoreMutation::PutSession(session)])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PromptRouteCancellation for SessionPromptTransport {
|
impl PromptRouteCancellation for SessionPromptTransport {
|
||||||
|
|
@ -1427,6 +1477,11 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
|
||||||
"-internal-pid-file={}",
|
"-internal-pid-file={}",
|
||||||
request.internal_pid_file.display()
|
request.internal_pid_file.display()
|
||||||
))
|
))
|
||||||
|
.argument("-env")
|
||||||
|
.argument(format!(
|
||||||
|
"LAZARET_SESSION_ID={}",
|
||||||
|
hex_lower(request.session_id.as_bytes())
|
||||||
|
))
|
||||||
.argument(hex_lower(request.container_id.as_bytes()))
|
.argument(hex_lower(request.container_id.as_bytes()))
|
||||||
.argument(SANDBOX_INIT_PATH)
|
.argument(SANDBOX_INIT_PATH)
|
||||||
.argument(PRIVATE_SESSION_EXEC_ROLE)
|
.argument(PRIVATE_SESSION_EXEC_ROLE)
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,31 @@ fn drive_terminal(
|
||||||
Err("terminal result deadline exhausted".into())
|
Err("terminal result deadline exhausted".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn acknowledge_one_chunk(
|
||||||
|
mailboxes: &Mailboxes,
|
||||||
|
coordinator: &mut EscalationCoordinator,
|
||||||
|
request: &mut ClientRequest<'_>,
|
||||||
|
now: Instant,
|
||||||
|
) -> TestResult<Vec<u8>> {
|
||||||
|
for _ in 0..500 {
|
||||||
|
coordinator.progress(request.request_id(), now)?;
|
||||||
|
match request.poll()? {
|
||||||
|
ClientEvent::Output {
|
||||||
|
sequence, bytes, ..
|
||||||
|
} => {
|
||||||
|
let ack = mailboxes.consume_ack(request.request_id(), sequence)?;
|
||||||
|
coordinator.acknowledge(request.request_id(), &ack, now)?;
|
||||||
|
return Ok(bytes);
|
||||||
|
}
|
||||||
|
ClientEvent::Terminal(_) => {
|
||||||
|
return Err("request completed before partial output".into());
|
||||||
|
}
|
||||||
|
ClientEvent::Pending => std::thread::sleep(Duration::from_millis(1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("partial output deadline exhausted".into())
|
||||||
|
}
|
||||||
|
|
||||||
fn completed_auto_and_developer_requests_preserve_attribution() -> TestResult {
|
fn completed_auto_and_developer_requests_preserve_attribution() -> TestResult {
|
||||||
for (seed, auto) in [(10, true), (30, false)] {
|
for (seed, auto) in [(10, true), (30, false)] {
|
||||||
let fixture = make_fixture(seed)?;
|
let fixture = make_fixture(seed)?;
|
||||||
|
|
@ -336,6 +361,19 @@ fn completed_auto_and_developer_requests_preserve_attribution() -> TestResult {
|
||||||
ApprovalSource::Developer
|
ApprovalSource::Developer
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
RegularFileMailbox::open(&mailboxes.outbox_path)?
|
||||||
|
.names()?
|
||||||
|
.contains(&lazaret_core::protocol::result_file_name(
|
||||||
|
request.request_id()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
coordinator.session_lost(fixture.session.session_binding().session_id)?;
|
||||||
|
assert!(
|
||||||
|
RegularFileMailbox::open(&mailboxes.outbox_path)?
|
||||||
|
.names()?
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
|
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -558,6 +596,73 @@ fn output_window_never_exceeds_eight_unacknowledged_chunks() -> TestResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn interrupted_execution_commits_acknowledged_output() -> TestResult {
|
||||||
|
let fixture = make_fixture(115)?;
|
||||||
|
let mailboxes = Mailboxes::create("cancel-after-output")?;
|
||||||
|
let client = mailboxes.client(&fixture)?;
|
||||||
|
let argv = vec![
|
||||||
|
mailboxes.command("/usr/bin/head", "head")?,
|
||||||
|
b"-c".to_vec(),
|
||||||
|
b"1000000".to_vec(),
|
||||||
|
b"/dev/zero".to_vec(),
|
||||||
|
];
|
||||||
|
let mut request = client.request(argv.clone(), "cancel after output".into())?;
|
||||||
|
let signed = mailboxes.consume_request(request.request_id())?;
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut coordinator =
|
||||||
|
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
|
||||||
|
coordinator.receive_auto_approved(
|
||||||
|
fixture.session.clone(),
|
||||||
|
&signed,
|
||||||
|
executable(&argv)?,
|
||||||
|
argv,
|
||||||
|
&mut RecordingOwnership::default(),
|
||||||
|
now,
|
||||||
|
)?;
|
||||||
|
assert!(!acknowledge_one_chunk(&mailboxes, &mut coordinator, &mut request, now)?.is_empty());
|
||||||
|
request.cancel()?;
|
||||||
|
let cancellation = RegularFileMailbox::open(&mailboxes.inbox_path)?.consume(
|
||||||
|
&lazaret_core::protocol::cancellation_file_name(request.request_id()),
|
||||||
|
MAX_SIGNED_RECORD_BYTES,
|
||||||
|
)?;
|
||||||
|
coordinator.cancel(request.request_id(), &cancellation)?;
|
||||||
|
assert!(
|
||||||
|
matches!(request.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::Cancelled)
|
||||||
|
);
|
||||||
|
|
||||||
|
let fixture = make_fixture(116)?;
|
||||||
|
let mailboxes = Mailboxes::create("timeout-after-output")?;
|
||||||
|
let client = mailboxes.client(&fixture)?;
|
||||||
|
let argv = vec![
|
||||||
|
mailboxes.command("/usr/bin/head", "head")?,
|
||||||
|
b"-c".to_vec(),
|
||||||
|
b"1000000".to_vec(),
|
||||||
|
b"/dev/zero".to_vec(),
|
||||||
|
];
|
||||||
|
let mut request = client.request(argv.clone(), "timeout after output".into())?;
|
||||||
|
let signed = mailboxes.consume_request(request.request_id())?;
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut coordinator =
|
||||||
|
mailboxes.coordinator(&fixture, Duration::from_millis(10), Duration::from_secs(2))?;
|
||||||
|
coordinator.receive_auto_approved(
|
||||||
|
fixture.session.clone(),
|
||||||
|
&signed,
|
||||||
|
executable(&argv)?,
|
||||||
|
argv,
|
||||||
|
&mut RecordingOwnership::default(),
|
||||||
|
start,
|
||||||
|
)?;
|
||||||
|
assert!(!acknowledge_one_chunk(&mailboxes, &mut coordinator, &mut request, start)?.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
coordinator.expire(start + Duration::from_millis(20))?,
|
||||||
|
vec![request.request_id()]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(request.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::TimedOut)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn replay_and_session_tuple_mismatch_refuse() -> TestResult {
|
fn replay_and_session_tuple_mismatch_refuse() -> TestResult {
|
||||||
let fixture = make_fixture(120)?;
|
let fixture = make_fixture(120)?;
|
||||||
let mailboxes = Mailboxes::create("replay")?;
|
let mailboxes = Mailboxes::create("replay")?;
|
||||||
|
|
@ -767,6 +872,7 @@ fn run() -> TestResult {
|
||||||
denial_failure_and_request_timeout_publish_terminal_outcomes()?;
|
denial_failure_and_request_timeout_publish_terminal_outcomes()?;
|
||||||
cancellation_session_loss_and_ack_timeout_reap_exact_children()?;
|
cancellation_session_loss_and_ack_timeout_reap_exact_children()?;
|
||||||
output_window_never_exceeds_eight_unacknowledged_chunks()?;
|
output_window_never_exceeds_eight_unacknowledged_chunks()?;
|
||||||
|
interrupted_execution_commits_acknowledged_output()?;
|
||||||
replay_and_session_tuple_mismatch_refuse()?;
|
replay_and_session_tuple_mismatch_refuse()?;
|
||||||
signed_cancellation_decision_and_result_tuple_mismatches_refuse()?;
|
signed_cancellation_decision_and_result_tuple_mismatches_refuse()?;
|
||||||
exact_descriptor_launcher_survives_path_replacement()?;
|
exact_descriptor_launcher_survives_path_replacement()?;
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,27 @@ impl RegularFileMailbox {
|
||||||
&self,
|
&self,
|
||||||
name: &std::ffi::OsStr,
|
name: &std::ffi::OsStr,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
|
) -> io::Result<MailboxEntryIdentity> {
|
||||||
|
self.publish_with_flags(name, bytes, RenameFlags::NOREPLACE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publishes one complete signed sender-owned record using an atomic rename.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses invalid names or incomplete durable publication.
|
||||||
|
pub fn publish_replacing(
|
||||||
|
&self,
|
||||||
|
name: &std::ffi::OsStr,
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> io::Result<MailboxEntryIdentity> {
|
||||||
|
self.publish_with_flags(name, bytes, RenameFlags::empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_with_flags(
|
||||||
|
&self,
|
||||||
|
name: &std::ffi::OsStr,
|
||||||
|
bytes: &[u8],
|
||||||
|
flags: RenameFlags,
|
||||||
) -> io::Result<MailboxEntryIdentity> {
|
) -> io::Result<MailboxEntryIdentity> {
|
||||||
validate_mailbox_name(name)?;
|
validate_mailbox_name(name)?;
|
||||||
let temporary_name = loop {
|
let temporary_name = loop {
|
||||||
|
|
@ -131,7 +152,7 @@ impl RegularFileMailbox {
|
||||||
&temporary_name,
|
&temporary_name,
|
||||||
self.directory.as_fd(),
|
self.directory.as_fd(),
|
||||||
name,
|
name,
|
||||||
RenameFlags::NOREPLACE,
|
flags,
|
||||||
)?;
|
)?;
|
||||||
self.directory.sync_all()?;
|
self.directory.sync_all()?;
|
||||||
Ok(MailboxEntryIdentity {
|
Ok(MailboxEntryIdentity {
|
||||||
|
|
@ -566,6 +587,24 @@ mod tests {
|
||||||
std::fs::remove_dir_all(directory).unwrap();
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sender_owned_mailbox_publication_atomically_replaces_stale_entry() {
|
||||||
|
let directory = test_directory("mailbox-sender-replace");
|
||||||
|
let mailbox = RegularFileMailbox::open(&directory).unwrap();
|
||||||
|
let name = std::ffi::OsStr::new("request.ready");
|
||||||
|
std::fs::write(directory.join(name), b"stale").unwrap();
|
||||||
|
mailbox.publish_replacing(name, b"signed").unwrap();
|
||||||
|
assert_eq!(std::fs::read(directory.join(name)).unwrap(), b"signed");
|
||||||
|
assert!(
|
||||||
|
mailbox
|
||||||
|
.names()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.all(|entry| !entry.as_encoded_bytes().ends_with(b".tmp"))
|
||||||
|
);
|
||||||
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mailbox_refuses_links_nonregular_and_oversize_records() {
|
fn mailbox_refuses_links_nonregular_and_oversize_records() {
|
||||||
let directory = test_directory("mailbox-hostile");
|
let directory = test_directory("mailbox-hostile");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue