Decouple client contracts and close private role registries

This commit is contained in:
ms2k 2026-07-19 03:44:30 +09:00
commit 22325aa76a
7 changed files with 1041 additions and 321 deletions

View file

@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use lazaret_host_coordinator::domain_store::StateResetScopeV1;
use lazaret_core::records::StateResetScopeV1;
use crate::ClientCommand;
use crate::state::{StateCommand, StateOperation};

View file

@ -8,19 +8,17 @@ use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::time::Duration;
use lazaret_control_client::{ControlClient, ControlClientError};
use lazaret_core::configuration::{
ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration,
};
use lazaret_host_coordinator::client_bootstrap::PRIVATE_CLIENT_BOOTSTRAP_ROLE;
use lazaret_host_coordinator::control::{ControlError, PublishedControlFace};
use lazaret_host_coordinator::waiter::{
BOOTSTRAP_WAITER_SOCKET_NAME, BootstrapWaitOutcome, BootstrapWaitRequest, read_frame,
write_frame,
use lazaret_core::protocol::{
BOOTSTRAP_WAITER_SOCKET_NAME, BootstrapWaitOutcome, BootstrapWaitRequest,
PRIVATE_CLIENT_BOOTSTRAP_ROLE, read_waiter_frame, write_waiter_frame,
};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_linux::process::{BootstrapChild, TargetCommand, child_broker};
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use rustix::fs::{Mode, OFlags, openat};
use rustix::pipe::{PipeFlags, pipe_with};
@ -80,7 +78,7 @@ impl CoordinatorClientBackend {
fn discover(&self) -> Result<Discovery, CoordinatorClientError> {
let configuration = self.configuration()?;
match PublishedControlFace::discover_confined(
match ControlClient::discover_confined(
configuration.runtime_domain_root(),
rustix::process::getuid().as_raw(),
) {
@ -96,7 +94,7 @@ impl CoordinatorClientBackend {
authority,
))))
}
Err(ControlError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
Err(ControlClientError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
Ok(Discovery::Absent)
}
Ok(_) | Err(_) => Ok(Discovery::Stale),
@ -292,17 +290,17 @@ impl CoordinatorClientBackend {
.set_read_timeout(Some(observation_remaining(observer)?))
.map_err(failure)
.map_err(ClientError::Backend)?;
write_frame(&mut stream, request).map_err(|error| {
write_waiter_frame(&mut stream, request).map_err(|error| {
ClientError::Backend(failure(format!("bootstrap waiter request failed: {error}")))
})?;
let first: BootstrapWaitOutcome = read_frame(&mut stream).map_err(|error| {
let first: BootstrapWaitOutcome = read_waiter_frame(&mut stream).map_err(|error| {
ClientError::Backend(failure(format!(
"bootstrap waiter admission failed: {error}"
)))
})?;
let mut action = observer.waiter(first, false);
if action == WaiterAction::AwaitTerminal {
action = match read_frame(&mut stream) {
action = match read_waiter_frame(&mut stream) {
Ok(outcome) => observer.waiter(outcome, true),
Err(_) => WaiterAction::RefuseFailed,
};
@ -465,7 +463,7 @@ pub(crate) fn load_configuration(
xdg_data_home: optional_environment_path("XDG_DATA_HOME"),
xdg_runtime_dir: required_environment_path("XDG_RUNTIME_DIR")?,
environment: environment(),
sandbox_layout: default_sandbox_layout()?,
sandbox_layout: lazaret_core::configuration::SandboxLayoutV1::default_layout()?,
})?)
}
@ -558,7 +556,7 @@ mod tests {
fs::create_dir(&runtime_path).unwrap();
fs::set_permissions(&runtime_path, fs::Permissions::from_mode(0o700)).unwrap();
assert!(
PublishedControlFace::discover_confined(
ControlClient::discover_confined(
runtime_configuration.runtime_domain_root(),
rustix::process::getuid().as_raw(),
)
@ -615,7 +613,7 @@ mod tests {
)
.unwrap();
let (authority, socket, observed) = PublishedControlFace::discover_confined(
let (authority, socket, observed) = ControlClient::discover_confined(
configuration.runtime_domain_root(),
rustix::process::getuid().as_raw(),
)
@ -690,7 +688,7 @@ mod tests {
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: default_sandbox_layout().unwrap(),
sandbox_layout: lazaret_core::configuration::SandboxLayoutV1::default_layout().unwrap(),
})
.unwrap()
}

View file

@ -123,12 +123,10 @@ impl<C: ObservationClock> DomainObservation<C> {
pub(crate) fn waiter(
&self,
outcome: lazaret_host_coordinator::waiter::BootstrapWaitOutcome,
outcome: lazaret_core::protocol::BootstrapWaitOutcome,
admitted: bool,
) -> WaiterAction {
use lazaret_host_coordinator::waiter::BootstrapWaitOutcome::{
Failed, Published, Refused, Waiting,
};
use lazaret_core::protocol::BootstrapWaitOutcome::{Failed, Published, Refused, Waiting};
if self.path != ObservationPath::WaitingClient {
return WaiterAction::RefuseProtocol;
}
@ -177,7 +175,7 @@ impl<C: ObservationClock> DomainObservation<C> {
#[cfg(test)]
mod tests {
use super::*;
use lazaret_host_coordinator::waiter::BootstrapWaitOutcome;
use lazaret_core::protocol::BootstrapWaitOutcome;
#[derive(Default)]
struct FakeClock {

View file

@ -5,6 +5,7 @@ use std::path::PathBuf;
use std::sync::Mutex;
use ed25519_dalek::SigningKey;
use lazaret_control_client::{ControlClient, ControlClientError, PendingClientEnrollment};
use lazaret_core::identity::{ClientId, SessionAnchorId, SessionId};
use lazaret_core::records::{
ClientEnrollmentResponse, ControlRequest, ControlResponse, DeveloperDecision,
@ -13,9 +14,6 @@ use lazaret_core::records::{
ProtocolPublicationIdentity, ProtocolVersion, SessionBinding, SessionExitOutcome,
SessionRegistrationRequest, TlsSubjectId,
};
use lazaret_host_coordinator::control::{
ControlError, PendingClientEnrollment, PublishedControlFace,
};
use sha2::{Digest, Sha256};
use crate::terminal::{HostTerminal, InteractiveSessionTransport, run_interactive_session};
@ -23,6 +21,7 @@ use crate::terminal::{HostTerminal, InteractiveSessionTransport, run_interactive
pub mod cli;
pub mod coordinator_backend;
mod domain_observation;
pub mod private_roles;
pub mod state;
pub mod terminal;
@ -59,7 +58,7 @@ pub enum ClientError<E> {
EmptyCommand,
Backend(E),
Io(io::Error),
Control(ControlError),
Control(ControlClientError),
Protocol(&'static str),
}
@ -302,15 +301,15 @@ pub fn execute_over_control_face_as<E>(
let cli_id = identity.cli_id;
let session_id = identity.session_id;
let pending = PendingClientEnrollment::create(generation.clone(), cli_id)?;
let anonymous = PublishedControlFace::client_config(&endpoint.publication, None)?;
let anonymous = ControlClient::config(&endpoint.publication, None)?;
let mut enrollment =
PublishedControlFace::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
PublishedControlFace::send(
ControlClient::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
ControlClient::send(
&mut enrollment,
&ControlRequest::Enroll(pending.request().clone()),
)?;
let ControlResponse::Enrolled(ClientEnrollmentResponse { credential, .. }) =
PublishedControlFace::receive(&mut enrollment)?
ControlClient::receive(&mut enrollment)?
else {
return Err(ClientError::Protocol(
"enrollment response was not Enrolled",
@ -331,21 +330,20 @@ pub fn execute_over_control_face_as<E>(
session_id,
session_anchor_public_key: identity.anchor.verifying_key().to_bytes().to_vec(),
};
let authenticated = PublishedControlFace::client_config(
let authenticated = ControlClient::config(
&endpoint.publication,
Some((&credential, pending.private_key())),
)?;
let mut stream =
PublishedControlFace::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
PublishedControlFace::send(
ControlClient::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
ControlClient::send(
&mut stream,
&ControlRequest::Register(SessionRegistrationRequest {
protocol_version: ProtocolVersion::V1,
session_binding: binding.clone(),
}),
)?;
let ControlResponse::Registered(registered) = PublishedControlFace::receive(&mut stream)?
else {
let ControlResponse::Registered(registered) = ControlClient::receive(&mut stream)? else {
return Err(ClientError::Protocol(
"registration response was not Registered",
));
@ -356,7 +354,7 @@ pub fn execute_over_control_face_as<E>(
));
}
endpoint.acknowledge_waiter_transfer()?;
PublishedControlFace::send(
ControlClient::send(
&mut stream,
&ControlRequest::Execute(NonInteractiveSessionRequest {
protocol_version: ProtocolVersion::V1,
@ -365,8 +363,8 @@ pub fn execute_over_control_face_as<E>(
}),
)?;
loop {
let message = PublishedControlFace::receive(&mut stream).map_err(|error| match error {
ControlError::Io(_) => ClientError::LostConnection,
let message = ControlClient::receive(&mut stream).map_err(|error| match error {
ControlClientError::Io(_) => ClientError::LostConnection,
other => ClientError::Control(other),
})?;
match message {
@ -412,15 +410,15 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
let cli_id = identity.cli_id;
let session_id = identity.session_id;
let pending = PendingClientEnrollment::create(generation.clone(), cli_id)?;
let anonymous = PublishedControlFace::client_config(&endpoint.publication, None)?;
let anonymous = ControlClient::config(&endpoint.publication, None)?;
let mut enrollment =
PublishedControlFace::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
PublishedControlFace::send(
ControlClient::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
ControlClient::send(
&mut enrollment,
&ControlRequest::Enroll(pending.request().clone()),
)?;
let ControlResponse::Enrolled(ClientEnrollmentResponse { credential, .. }) =
PublishedControlFace::receive(&mut enrollment)?
ControlClient::receive(&mut enrollment)?
else {
return Err(ClientError::Protocol(
"enrollment response was not Enrolled",
@ -440,21 +438,20 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
session_id,
session_anchor_public_key: identity.anchor.verifying_key().to_bytes().to_vec(),
};
let authenticated = PublishedControlFace::client_config(
let authenticated = ControlClient::config(
&endpoint.publication,
Some((&credential, pending.private_key())),
)?;
let mut stream =
PublishedControlFace::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
PublishedControlFace::send(
ControlClient::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
ControlClient::send(
&mut stream,
&ControlRequest::Register(SessionRegistrationRequest {
protocol_version: ProtocolVersion::V1,
session_binding: binding.clone(),
}),
)?;
let ControlResponse::Registered(registered) = PublishedControlFace::receive(&mut stream)?
else {
let ControlResponse::Registered(registered) = ControlClient::receive(&mut stream)? else {
return Err(ClientError::Protocol(
"registration response was not Registered",
));
@ -481,7 +478,7 @@ impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
&mut self,
request: InteractiveSessionRequest,
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
PublishedControlFace::send(
ControlClient::send(
&mut self.stream,
&ControlRequest::ExecuteInteractive(request),
)?;
@ -492,7 +489,7 @@ impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
&mut self,
event: InteractiveSessionEvent,
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
PublishedControlFace::send(&mut self.stream, &event).map_err(map_control_connection)?;
ControlClient::send(&mut self.stream, &event).map_err(map_control_connection)?;
receive_interactive_batch(&mut self.stream)
}
}
@ -500,12 +497,12 @@ impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
fn receive_interactive_batch(
stream: &mut impl std::io::Read,
) -> Result<InteractiveSessionBatch, ClientError<std::convert::Infallible>> {
PublishedControlFace::receive(stream).map_err(map_control_connection)
ControlClient::receive(stream).map_err(map_control_connection)
}
fn map_control_connection(error: ControlError) -> ClientError<std::convert::Infallible> {
fn map_control_connection(error: ControlClientError) -> ClientError<std::convert::Infallible> {
match error {
ControlError::Io(_) => ClientError::LostConnection,
ControlClientError::Io(_) => ClientError::LostConnection,
other => ClientError::Control(other),
}
}
@ -547,8 +544,8 @@ impl<E> From<io::Error> for ClientError<E> {
}
}
impl<E> From<ControlError> for ClientError<E> {
fn from(error: ControlError) -> Self {
impl<E> From<ControlClientError> for ClientError<E> {
fn from(error: ControlClientError) -> Self {
Self::Control(error)
}
}

View file

@ -1,8 +1,8 @@
fn main() {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
if let Some(result) = lazaret_linux::process::dispatch_private_process_role(&arguments) {
if let Err(error) = result {
eprintln!("private process role failed: {error}");
if let Some(outcome) = lazaret_client::private_roles::REGISTRY.dispatch(&arguments) {
if let Err(error) = outcome.result {
eprintln!("private role {} failed: {error}", outcome.name);
std::process::exit(1);
}
return;

View file

@ -0,0 +1,19 @@
use lazaret_linux::private_role::{PrivateRole, PrivateRoleRegistry};
use lazaret_linux::process::{PRIVATE_PROCESS_ROLE, dispatch_private_process_role};
static ROLES: [PrivateRole; 1] = [PrivateRole::new(
PRIVATE_PROCESS_ROLE,
dispatch_private_process_role,
)];
pub static REGISTRY: PrivateRoleRegistry = PrivateRoleRegistry::new(&ROLES);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_declares_only_process_acquisition() {
assert_eq!(REGISTRY.names(), [PRIVATE_PROCESS_ROLE]);
}
}

View file

@ -1,9 +1,9 @@
use std::io::{self, Write};
use std::os::fd::AsFd;
use lazaret_core::records::Domain;
use lazaret_host_coordinator::domain_store::{HostCoordinatorDomainStore, StateResetScopeV1};
use lazaret_host_coordinator::state_maintenance::{
use lazaret_core::records::{Domain, StateResetScopeV1};
use lazaret_domain_state::domain_store::HostCoordinatorDomainStore;
use lazaret_domain_state::state_maintenance::{
DomainStateReport, StateAnchorEntryKind, inspect_absent_domain_state, inspect_domain_state,
reset_domain_state,
};