Implement supervised rootless network readiness
This commit is contained in:
parent
407b75b1ff
commit
ba6a075839
4 changed files with 2917 additions and 54 deletions
|
|
@ -3,5 +3,6 @@
|
|||
pub mod control;
|
||||
pub mod domain_store;
|
||||
pub mod escalation;
|
||||
pub mod network;
|
||||
pub mod runtime;
|
||||
pub mod state_anchor;
|
||||
|
|
|
|||
482
crates/lazaret-host-coordinator/src/network.rs
Normal file
482
crates/lazaret-host-coordinator/src/network.rs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use lazaret_core::identity::{
|
||||
DerivedPolicyDigest, GenerationId, NamespaceIdentity, NetworkSidecarId, PidfsProcessIdentityV1,
|
||||
};
|
||||
use lazaret_core::network::{
|
||||
CanonicalDerivedNetworkPolicy, ReachabilityExpectation, ReadinessExpectation,
|
||||
accept_network_readiness, validate_network_attestation,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
HelperOwnership, HostProcessOwnershipSlot, NetworkOwnership, NetworkReachabilityProbe,
|
||||
NetworkReadinessAttestation, NetworkReadinessEvidence, Participant, ParticipantOwnership,
|
||||
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
|
||||
StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::filesystem::namespace_identity;
|
||||
|
||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkCoordinatorError {
|
||||
Store(DomainStoreError),
|
||||
Io(std::io::Error),
|
||||
InvalidState(&'static str),
|
||||
InvalidEvidence(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for NetworkCoordinatorError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Store(error) => {
|
||||
write!(formatter, "Network Domain Store operation failed: {error}")
|
||||
}
|
||||
Self::Io(error) => write!(formatter, "Network host observation failed: {error}"),
|
||||
Self::InvalidState(detail) => write!(formatter, "Network state refused: {detail}"),
|
||||
Self::InvalidEvidence(detail) => {
|
||||
write!(formatter, "Network evidence refused: {detail}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NetworkCoordinatorError {}
|
||||
|
||||
impl From<DomainStoreError> for NetworkCoordinatorError {
|
||||
fn from(error: DomainStoreError) -> Self {
|
||||
Self::Store(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for NetworkCoordinatorError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetworkCoordinator<'store> {
|
||||
domain_store: &'store HostCoordinatorDomainStore,
|
||||
}
|
||||
|
||||
impl<'store> NetworkCoordinator<'store> {
|
||||
#[must_use]
|
||||
pub const fn new(domain_store: &'store HostCoordinatorDomainStore) -> Self {
|
||||
Self { domain_store }
|
||||
}
|
||||
|
||||
/// Persists independent helper intent before launch and the observed Network RS authority.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a missing generation, changed identity, or an already-different role snapshot.
|
||||
pub fn begin_helper_intent(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
network_sidecar_id: NetworkSidecarId,
|
||||
runtime_supervisor: PidfsProcessIdentityV1,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
let snapshot = self.domain_store.snapshot()?;
|
||||
let generation = snapshot
|
||||
.generations
|
||||
.iter()
|
||||
.find(|generation| generation.generation_id == generation_id)
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"helper generation is absent",
|
||||
))?;
|
||||
if generation.network_sidecar_id != network_sidecar_id {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper Network Sidecar identity mismatches generation",
|
||||
));
|
||||
}
|
||||
let intent = helper_participant(
|
||||
generation_id,
|
||||
ParticipantState::Intent,
|
||||
HelperOwnership {
|
||||
network_sidecar_id: Some(network_sidecar_id),
|
||||
connectivity_helper: PersistedHostProcessOwnership::unassigned(),
|
||||
runtime_supervisor: PersistedHostProcessOwnership::committed(runtime_supervisor),
|
||||
},
|
||||
)?;
|
||||
put_initial_or_exact(self.domain_store, &snapshot.participants, intent)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses a non-helper tuple, absent intent, changed RS, or skipped acquisition phase.
|
||||
pub fn stage_helper(
|
||||
&self,
|
||||
staged: StagedProcessOwnership,
|
||||
runtime_supervisor: PidfsProcessIdentityV1,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
self.require_staged(staged)?;
|
||||
let current = self.participant(staged.generation_id(), ParticipantRole::Helper)?;
|
||||
if current.state() != ParticipantState::Intent {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper acquisition is not in Intent",
|
||||
));
|
||||
}
|
||||
let Some(ParticipantOwnership::Helper(mut ownership)) = current.ownership().cloned() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
if ownership.runtime_supervisor
|
||||
!= PersistedHostProcessOwnership::committed(runtime_supervisor)
|
||||
|| !ownership.connectivity_helper.is_unassigned()
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership changed before staging",
|
||||
));
|
||||
}
|
||||
ownership.connectivity_helper = PersistedHostProcessOwnership::staged(staged.identity());
|
||||
self.put(helper_participant(
|
||||
staged.generation_id(),
|
||||
ParticipantState::Intent,
|
||||
ownership,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses acknowledgment unless the exact helper tuple is durable.
|
||||
pub fn acknowledge_helper(
|
||||
&self,
|
||||
staged: StagedProcessOwnership,
|
||||
) -> Result<ProcessOwnershipCommitAck, NetworkCoordinatorError> {
|
||||
self.require_staged(staged)?;
|
||||
let current = self.participant(staged.generation_id(), ParticipantRole::Helper)?;
|
||||
let Some(ParticipantOwnership::Helper(ownership)) = current.ownership() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
if ownership.connectivity_helper != PersistedHostProcessOwnership::staged(staged.identity())
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"staged helper ownership is not durable",
|
||||
));
|
||||
}
|
||||
Ok(ProcessOwnershipCommitAck::v1(staged))
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses a changed helper tuple or incomplete supervising ownership.
|
||||
pub fn commit_helper(
|
||||
&self,
|
||||
staged: StagedProcessOwnership,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
self.require_staged(staged)?;
|
||||
let current = self.participant(staged.generation_id(), ParticipantRole::Helper)?;
|
||||
let Some(ParticipantOwnership::Helper(mut ownership)) = current.ownership().cloned() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
if ownership.connectivity_helper != PersistedHostProcessOwnership::staged(staged.identity())
|
||||
|| !ownership.runtime_supervisor.is_current_committed()
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper readiness does not match committed ownership",
|
||||
));
|
||||
}
|
||||
ownership.connectivity_helper = PersistedHostProcessOwnership::committed(staged.identity());
|
||||
self.put(helper_participant(
|
||||
staged.generation_id(),
|
||||
ParticipantState::Committed,
|
||||
ownership,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Independently observes the exact RS network namespace and persists Network intent.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a missing generation, changed policy, namespace, or Runtime Supervisor identity.
|
||||
pub fn begin_network_intent(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
policy_digest: DerivedPolicyDigest,
|
||||
runtime_supervisor: PidfsProcessIdentityV1,
|
||||
) -> Result<NamespaceIdentity, NetworkCoordinatorError> {
|
||||
let namespace = observe_process_network_namespace(runtime_supervisor)?;
|
||||
let snapshot = self.domain_store.snapshot()?;
|
||||
if !snapshot
|
||||
.generations
|
||||
.iter()
|
||||
.any(|generation| generation.generation_id == generation_id)
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network generation is absent",
|
||||
));
|
||||
}
|
||||
let intent = network_participant(
|
||||
generation_id,
|
||||
ParticipantState::Intent,
|
||||
NetworkOwnership {
|
||||
derived_policy_digest: Some(policy_digest),
|
||||
readiness_fingerprint: None,
|
||||
readiness_evidence: None,
|
||||
namespace: Some(namespace),
|
||||
runtime_supervisor: PersistedHostProcessOwnership::committed(runtime_supervisor),
|
||||
},
|
||||
)?;
|
||||
put_initial_or_exact(self.domain_store, &snapshot.participants, intent)?;
|
||||
Ok(namespace)
|
||||
}
|
||||
|
||||
/// Independently validates RS attestation before allowing attached runsc acquisition.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses absent committed helper/RS ownership, namespace mutation, or identity mismatch.
|
||||
pub fn validate_attestation(
|
||||
&self,
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
attestation: &NetworkReadinessAttestation,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
let snapshot = self.domain_store.snapshot()?;
|
||||
let generation = snapshot
|
||||
.generations
|
||||
.iter()
|
||||
.find(|generation| generation.generation_id == attestation.generation_id)
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"attestation generation is absent",
|
||||
))?;
|
||||
let network = self.participant(attestation.generation_id, ParticipantRole::Network)?;
|
||||
let Some(ParticipantOwnership::Network(network)) = network.ownership() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
let namespace = network
|
||||
.namespace
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"network namespace evidence is absent",
|
||||
))?;
|
||||
if observe_process_network_namespace(attestation.runtime_supervisor)? != namespace {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor network namespace changed",
|
||||
));
|
||||
}
|
||||
let runtime_supervisor = *network
|
||||
.runtime_supervisor
|
||||
.serviceable_pidfs()
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("Network RS is not committed"))?;
|
||||
let helper = self.participant(attestation.generation_id, ParticipantRole::Helper)?;
|
||||
let Some(ParticipantOwnership::Helper(helper)) = helper.ownership() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
let connectivity_helper = *helper
|
||||
.connectivity_helper
|
||||
.serviceable_pidfs()
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("helper is not committed"))?;
|
||||
let expectation = ReadinessExpectation {
|
||||
domain_id: snapshot.domain.domain_id,
|
||||
generation_id: generation.generation_id,
|
||||
configuration_fingerprint: generation.configuration_fingerprint,
|
||||
network_sidecar_id: generation.network_sidecar_id,
|
||||
enforcement_namespace: namespace,
|
||||
runtime_supervisor: &runtime_supervisor,
|
||||
connectivity_helper: &connectivity_helper,
|
||||
};
|
||||
validate_network_attestation(policy, &expectation, attestation)
|
||||
.map_err(|error| NetworkCoordinatorError::InvalidEvidence(error.to_string()))
|
||||
}
|
||||
|
||||
/// Validates the exact attestation and sandbox-view probe before durable readiness.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses incomplete helper ownership, mutation, failed probe, or any generation mismatch.
|
||||
pub fn commit_readiness(
|
||||
&self,
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
attestation: NetworkReadinessAttestation,
|
||||
probe: NetworkReachabilityProbe,
|
||||
expected_reachability: &ReachabilityExpectation,
|
||||
) -> Result<NetworkReadinessEvidence, NetworkCoordinatorError> {
|
||||
let generation_id = attestation.generation_id;
|
||||
let snapshot = self.domain_store.snapshot()?;
|
||||
let generation = snapshot
|
||||
.generations
|
||||
.iter()
|
||||
.find(|generation| generation.generation_id == generation_id)
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"readiness generation is absent",
|
||||
))?;
|
||||
let network = self.participant(generation_id, ParticipantRole::Network)?;
|
||||
let Some(ParticipantOwnership::Network(ownership)) = network.ownership().cloned() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
let mut ownership = *ownership;
|
||||
if network.state() != ParticipantState::Intent
|
||||
|| ownership.derived_policy_digest != Some(policy.digest())
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network policy intent is absent or changed",
|
||||
));
|
||||
}
|
||||
let namespace = ownership
|
||||
.namespace
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"network namespace evidence is absent",
|
||||
))?;
|
||||
let runtime_supervisor = *ownership
|
||||
.runtime_supervisor
|
||||
.serviceable_pidfs()
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("Network RS is not committed"))?;
|
||||
let helper_participant = self.participant(generation_id, ParticipantRole::Helper)?;
|
||||
let Some(ParticipantOwnership::Helper(helper)) = helper_participant.ownership() else {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership shape is invalid",
|
||||
));
|
||||
};
|
||||
if helper_participant.state() != ParticipantState::Committed
|
||||
|| helper.runtime_supervisor.serviceable_pidfs().ok() != Some(&runtime_supervisor)
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"helper and supervising RS are not committed",
|
||||
));
|
||||
}
|
||||
let connectivity_helper = *helper
|
||||
.connectivity_helper
|
||||
.serviceable_pidfs()
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("helper is not committed"))?;
|
||||
let expectation = ReadinessExpectation {
|
||||
domain_id: snapshot.domain.domain_id,
|
||||
generation_id,
|
||||
configuration_fingerprint: generation.configuration_fingerprint,
|
||||
network_sidecar_id: generation.network_sidecar_id,
|
||||
enforcement_namespace: namespace,
|
||||
runtime_supervisor: &runtime_supervisor,
|
||||
connectivity_helper: &connectivity_helper,
|
||||
};
|
||||
let evidence = accept_network_readiness(
|
||||
policy,
|
||||
&expectation,
|
||||
attestation,
|
||||
probe,
|
||||
expected_reachability,
|
||||
)
|
||||
.map_err(|error| NetworkCoordinatorError::InvalidEvidence(error.to_string()))?;
|
||||
ownership.readiness_fingerprint = Some(evidence.readiness_fingerprint);
|
||||
ownership.readiness_evidence = Some(evidence.clone());
|
||||
self.put(network_participant(
|
||||
generation_id,
|
||||
ParticipantState::Committed,
|
||||
ownership,
|
||||
)?)?;
|
||||
Ok(evidence)
|
||||
}
|
||||
|
||||
fn require_staged(
|
||||
&self,
|
||||
staged: StagedProcessOwnership,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
let snapshot = self.domain_store.snapshot()?;
|
||||
let generation = snapshot
|
||||
.generations
|
||||
.iter()
|
||||
.find(|generation| generation.generation_id == staged.generation_id())
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"staged helper generation is absent",
|
||||
))?;
|
||||
if staged.domain_id() != snapshot.domain.domain_id
|
||||
|| staged.coordinator_id() != generation.coordinator_id
|
||||
|| staged.slot() != HostProcessOwnershipSlot::HelperConnectivityHelper
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"staged helper tuple mismatches generation",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn participant(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
role: ParticipantRole,
|
||||
) -> Result<Participant, NetworkCoordinatorError> {
|
||||
self.domain_store
|
||||
.snapshot()?
|
||||
.participants
|
||||
.into_iter()
|
||||
.find(|participant| {
|
||||
participant.generation_id() == generation_id && participant.role() == role
|
||||
})
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"network participant is absent",
|
||||
))
|
||||
}
|
||||
|
||||
fn put(&self, participant: Participant) -> Result<(), NetworkCoordinatorError> {
|
||||
self.domain_store
|
||||
.apply(&[DomainStoreMutation::PutParticipant(participant)])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn put_initial_or_exact(
|
||||
store: &HostCoordinatorDomainStore,
|
||||
participants: &[Participant],
|
||||
intent: Participant,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
match participants.iter().find(|participant| {
|
||||
participant.generation_id() == intent.generation_id() && participant.role() == intent.role()
|
||||
}) {
|
||||
None => store.apply(&[
|
||||
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
||||
intent.generation_id(),
|
||||
intent.role(),
|
||||
)),
|
||||
DomainStoreMutation::PutParticipant(intent),
|
||||
])?,
|
||||
Some(current) if current == &intent => {}
|
||||
Some(_) => {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network role intent already differs",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn network_participant(
|
||||
generation_id: GenerationId,
|
||||
state: ParticipantState,
|
||||
ownership: NetworkOwnership,
|
||||
) -> Result<Participant, NetworkCoordinatorError> {
|
||||
Participant::snapshot(
|
||||
generation_id,
|
||||
ParticipantRole::Network,
|
||||
state,
|
||||
Some(ParticipantOwnership::Network(Box::new(ownership))),
|
||||
)
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("network participant shape is invalid"))
|
||||
}
|
||||
|
||||
fn helper_participant(
|
||||
generation_id: GenerationId,
|
||||
state: ParticipantState,
|
||||
ownership: HelperOwnership,
|
||||
) -> Result<Participant, NetworkCoordinatorError> {
|
||||
Participant::snapshot(
|
||||
generation_id,
|
||||
ParticipantRole::Helper,
|
||||
state,
|
||||
Some(ParticipantOwnership::Helper(ownership)),
|
||||
)
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("helper participant shape is invalid"))
|
||||
}
|
||||
|
||||
fn observe_process_network_namespace(
|
||||
runtime_supervisor: PidfsProcessIdentityV1,
|
||||
) -> Result<NamespaceIdentity, NetworkCoordinatorError> {
|
||||
let path = PathBuf::from(format!(
|
||||
"/proc/{}/ns/net",
|
||||
runtime_supervisor.host_pid().get()
|
||||
));
|
||||
Ok(namespace_identity(&path)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
244
crates/lazaret-host-coordinator/src/network/tests.rs
Normal file
244
crates/lazaret-host-coordinator/src/network/tests.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
use std::fs::{File, OpenOptions};
|
||||
|
||||
use lazaret_core::identity::{
|
||||
CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId, HostPath, NamespaceIdentity,
|
||||
NetworkSidecarId, PidfsProcessIdentityV1, SandboxId,
|
||||
};
|
||||
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation, derive_policy};
|
||||
use lazaret_core::records::{
|
||||
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction,
|
||||
DeniedDestinationObservation, DerivedNetworkPolicyFields, DnsObservation, Domain,
|
||||
DurableDecision, Generation, HostProcessOwnershipSlot, IpAddress, IpNetwork,
|
||||
NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation,
|
||||
NetworkReadinessAttestationFields, ParticipantOwnership, ParticipantRole, ParticipantState,
|
||||
PermittedDestinationObservation, PolicyAction, PolicyDelivery, PrefixRule, Resolver,
|
||||
StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
|
||||
use lazaret_linux::process::current_process_identity;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Fixture {
|
||||
_tree: TempDir,
|
||||
store: HostCoordinatorDomainStore,
|
||||
generation: Generation,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let domain = Domain {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
profile_name: "network-test".into(),
|
||||
canonical_workspace: HostPath::new(b"/workspace".to_vec()),
|
||||
};
|
||||
let generation = Generation {
|
||||
domain_id: domain.domain_id,
|
||||
generation_id: GenerationId::new(7),
|
||||
configuration_fingerprint: lazaret_core::identity::ConfigurationFingerprint::new(
|
||||
[2; 32],
|
||||
),
|
||||
coordinator_id: CoordinatorId::new([3; 32]),
|
||||
network_sidecar_id: NetworkSidecarId::new([4; 32]),
|
||||
sandbox_id: SandboxId::new([5; 32]),
|
||||
durable_decision: DurableDecision::None,
|
||||
};
|
||||
let lock = try_exclusive_flock(lock_file(&tree.path().join("domain.lock"))).unwrap();
|
||||
let store =
|
||||
HostCoordinatorDomainStore::open(lock, &tree.path().join("domain.redb"), &domain)
|
||||
.unwrap();
|
||||
store
|
||||
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
|
||||
.unwrap();
|
||||
Self {
|
||||
_tree: tree,
|
||||
store,
|
||||
generation,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_roles(&self) -> (CanonicalDerivedNetworkPolicy, PidfsProcessIdentityV1) {
|
||||
let policy = policy();
|
||||
let runtime = current_process_identity().unwrap();
|
||||
let coordinator = NetworkCoordinator::new(&self.store);
|
||||
coordinator
|
||||
.begin_helper_intent(
|
||||
self.generation.generation_id,
|
||||
self.generation.network_sidecar_id,
|
||||
runtime,
|
||||
)
|
||||
.unwrap();
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
self.generation.domain_id,
|
||||
self.generation.generation_id,
|
||||
self.generation.coordinator_id,
|
||||
HostProcessOwnershipSlot::HelperConnectivityHelper,
|
||||
runtime,
|
||||
);
|
||||
coordinator.stage_helper(staged, runtime).unwrap();
|
||||
assert!(
|
||||
coordinator
|
||||
.acknowledge_helper(staged)
|
||||
.unwrap()
|
||||
.acknowledges(&staged)
|
||||
);
|
||||
coordinator.commit_helper(staged).unwrap();
|
||||
coordinator
|
||||
.begin_network_intent(self.generation.generation_id, policy.digest(), runtime)
|
||||
.unwrap();
|
||||
(policy, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_probe_leaves_network_intent_uncommitted() {
|
||||
let fixture = Fixture::new();
|
||||
let (policy, runtime) = fixture.prepare_roles();
|
||||
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
|
||||
let attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
|
||||
let probe = NetworkReachabilityProbe {
|
||||
domain_id: fixture.generation.domain_id,
|
||||
generation_id: fixture.generation.generation_id,
|
||||
enforcement_namespace: namespace,
|
||||
denied_destinations: vec![DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 1, 2, 3]),
|
||||
unreachable: false,
|
||||
}],
|
||||
permitted_destinations: vec![PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([1, 1, 1, 1]),
|
||||
reachable: true,
|
||||
}],
|
||||
dns_observations: vec![DnsObservation {
|
||||
resolver: IpAddress::V4([8, 8, 8, 8]),
|
||||
query_name: "example.com".into(),
|
||||
resolved_addresses: vec![IpAddress::V4([93, 184, 216, 34])],
|
||||
}],
|
||||
};
|
||||
let result = NetworkCoordinator::new(&fixture.store).commit_readiness(
|
||||
&policy,
|
||||
attestation,
|
||||
probe,
|
||||
&reachability(),
|
||||
);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NetworkCoordinatorError::InvalidEvidence(_))
|
||||
));
|
||||
let participant = fixture
|
||||
.store
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.participants
|
||||
.into_iter()
|
||||
.find(|participant| participant.role() == ParticipantRole::Network)
|
||||
.unwrap();
|
||||
assert_eq!(participant.state(), ParticipantState::Intent);
|
||||
let Some(ParticipantOwnership::Network(ownership)) = participant.ownership() else {
|
||||
panic!("network ownership missing");
|
||||
};
|
||||
assert!(ownership.readiness_evidence.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independently_observed_namespace_refuses_attestation_mutation() {
|
||||
let fixture = Fixture::new();
|
||||
let (policy, runtime) = fixture.prepare_roles();
|
||||
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
|
||||
let mut attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
|
||||
attestation.enforcement_namespace.inode =
|
||||
lazaret_core::identity::InodeId::new(attestation.enforcement_namespace.inode.get() + 1);
|
||||
assert!(matches!(
|
||||
NetworkCoordinator::new(&fixture.store).validate_attestation(&policy, &attestation),
|
||||
Err(NetworkCoordinatorError::InvalidState(_))
|
||||
| Err(NetworkCoordinatorError::InvalidEvidence(_))
|
||||
));
|
||||
}
|
||||
|
||||
fn policy() -> CanonicalDerivedNetworkPolicy {
|
||||
derive_policy(DerivedNetworkPolicyFields {
|
||||
address_families: vec![
|
||||
AddressFamilyDisposition {
|
||||
family: AddressFamily::Ipv4,
|
||||
disposition: AddressFamilyDispositionValue::Enabled,
|
||||
},
|
||||
AddressFamilyDisposition {
|
||||
family: AddressFamily::Ipv6,
|
||||
disposition: AddressFamilyDispositionValue::Disabled,
|
||||
},
|
||||
],
|
||||
default_actions: vec![DefaultAction {
|
||||
family: AddressFamily::Ipv4,
|
||||
action: PolicyAction::Allow,
|
||||
}],
|
||||
prefix_rules: [
|
||||
([10, 0, 0, 0], 8),
|
||||
([172, 16, 0, 0], 12),
|
||||
([192, 168, 0, 0], 16),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(address, prefix_length)| PrefixRule {
|
||||
family: AddressFamily::Ipv4,
|
||||
network: IpNetwork {
|
||||
address: IpAddress::V4(address),
|
||||
prefix_length,
|
||||
},
|
||||
action: PolicyAction::Deny,
|
||||
})
|
||||
.collect(),
|
||||
resolvers: vec![Resolver {
|
||||
family: AddressFamily::Ipv4,
|
||||
address: IpAddress::V4([8, 8, 8, 8]),
|
||||
}],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn attestation(
|
||||
generation: &Generation,
|
||||
digest: DerivedPolicyDigest,
|
||||
namespace: NamespaceIdentity,
|
||||
runtime: PidfsProcessIdentityV1,
|
||||
) -> NetworkReadinessAttestation {
|
||||
NetworkReadinessAttestation::v1(NetworkReadinessAttestationFields {
|
||||
domain_id: generation.domain_id,
|
||||
generation_id: generation.generation_id,
|
||||
configuration_fingerprint: generation.configuration_fingerprint,
|
||||
network_sidecar_id: generation.network_sidecar_id,
|
||||
derived_policy_digest: digest,
|
||||
enforcement_namespace: namespace,
|
||||
runtime_supervisor: runtime,
|
||||
connectivity_helper: runtime,
|
||||
observation: NetworkObservation {
|
||||
policy_delivery: PolicyDelivery::SealedMemfd,
|
||||
helper_ready: true,
|
||||
observed_routes: vec![IpNetwork {
|
||||
address: IpAddress::V4([10, 0, 0, 0]),
|
||||
prefix_length: 8,
|
||||
}],
|
||||
routes_attested: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn reachability() -> ReachabilityExpectation {
|
||||
ReachabilityExpectation {
|
||||
denied_destinations: vec![IpAddress::V4([10, 1, 2, 3])],
|
||||
permitted_destinations: vec![IpAddress::V4([1, 1, 1, 1])],
|
||||
dns_queries: vec![DnsQueryExpectation {
|
||||
resolver: IpAddress::V4([8, 8, 8, 8]),
|
||||
query_name: "example.com".into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_file(path: &std::path::Path) -> File {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(path)
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -5,15 +5,17 @@ use std::path::Path;
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazaret_core::identity::{
|
||||
BundleDigest, ContainerId, GenerationId, HostPath, PidfsProcessIdentityV1,
|
||||
BundleDigest, ContainerId, GenerationId, HostPath, NamespaceIdentity, PidfsProcessIdentityV1,
|
||||
};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::records::{
|
||||
HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole, ParticipantState,
|
||||
PersistedHostProcessOwnership, ProcessOwnershipCommitAck, SandboxOwnership,
|
||||
StagedProcessOwnership,
|
||||
HostProcessOwnershipSlot, NetworkReadinessEvidence, Participant, ParticipantOwnership,
|
||||
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
|
||||
SandboxOwnership, StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::ipc::private_stream_pair;
|
||||
use lazaret_linux::ipc::private_seqpacket_pair;
|
||||
use lazaret_linux::process::{RetainedProcess, child_broker, current_process_identity};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::supervision::{
|
||||
AttachedRunscSignal, CoordinatorChannel, RuntimeSupervisorLaunch, SupervisorEvent,
|
||||
};
|
||||
|
|
@ -21,12 +23,14 @@ use rustix::pipe::{PipeFlags, pipe_with};
|
|||
use rustix::process::Signal;
|
||||
|
||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||
use crate::network::{NetworkCoordinator, NetworkCoordinatorError};
|
||||
|
||||
const SUPERVISOR_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxCoordinatorError {
|
||||
Store(DomainStoreError),
|
||||
Network(NetworkCoordinatorError),
|
||||
Io(io::Error),
|
||||
InvalidState(&'static str),
|
||||
}
|
||||
|
|
@ -37,6 +41,7 @@ impl fmt::Display for SandboxCoordinatorError {
|
|||
Self::Store(error) => {
|
||||
write!(formatter, "Sandbox Domain Store operation failed: {error}")
|
||||
}
|
||||
Self::Network(error) => write!(formatter, "Sandbox network operation failed: {error}"),
|
||||
Self::Io(error) => write!(formatter, "Sandbox process operation failed: {error}"),
|
||||
Self::InvalidState(detail) => write!(formatter, "Sandbox state refused: {detail}"),
|
||||
}
|
||||
|
|
@ -47,6 +52,7 @@ impl std::error::Error for SandboxCoordinatorError {
|
|||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Store(error) => Some(error),
|
||||
Self::Network(error) => Some(error),
|
||||
Self::Io(error) => Some(error),
|
||||
Self::InvalidState(_) => None,
|
||||
}
|
||||
|
|
@ -65,6 +71,12 @@ impl From<io::Error> for SandboxCoordinatorError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<NetworkCoordinatorError> for SandboxCoordinatorError {
|
||||
fn from(error: NetworkCoordinatorError) -> Self {
|
||||
Self::Network(error)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SandboxCoordinator<'store> {
|
||||
domain_store: &'store HostCoordinatorDomainStore,
|
||||
}
|
||||
|
|
@ -351,9 +363,16 @@ pub struct AwaitingAttachedRunsc {
|
|||
control: Option<CoordinatorChannel>,
|
||||
lifetime: Option<std::os::fd::OwnedFd>,
|
||||
staged: Option<StagedProcessOwnership>,
|
||||
helper_staged: Option<StagedProcessOwnership>,
|
||||
network_evidence: Option<NetworkReadinessEvidence>,
|
||||
}
|
||||
|
||||
impl AwaitingAttachedRunsc {
|
||||
#[must_use]
|
||||
pub fn runtime_supervisor_identity(&self) -> Option<PidfsProcessIdentityV1> {
|
||||
self.supervisor.as_ref().map(RetainedProcess::identity)
|
||||
}
|
||||
|
||||
/// Launches the RS through the HC broker and commits its staged ownership before execution.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -361,9 +380,39 @@ impl AwaitingAttachedRunsc {
|
|||
pub fn launch(
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
launch: &RuntimeSupervisorLaunch,
|
||||
) -> Result<Self, SandboxCoordinatorError> {
|
||||
if launch.network_sidecar_id().is_some() {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"networked Runtime Supervisor requires helper intent",
|
||||
));
|
||||
}
|
||||
Self::launch_inner(coordinator, None, launch)
|
||||
}
|
||||
|
||||
/// Launches the RS only after independent durable helper intent is recorded.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a non-network launch or any sandbox/helper staging failure.
|
||||
pub fn launch_networked(
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
network: &NetworkCoordinator<'_>,
|
||||
launch: &RuntimeSupervisorLaunch,
|
||||
) -> Result<Self, SandboxCoordinatorError> {
|
||||
if launch.network_sidecar_id().is_none() {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor launch has no network policy",
|
||||
));
|
||||
}
|
||||
Self::launch_inner(coordinator, Some(network), launch)
|
||||
}
|
||||
|
||||
fn launch_inner(
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
network: Option<&NetworkCoordinator<'_>>,
|
||||
launch: &RuntimeSupervisorLaunch,
|
||||
) -> Result<Self, SandboxCoordinatorError> {
|
||||
let parent = current_process_identity()?;
|
||||
let (coordinator_control, supervisor_control) = private_stream_pair()?;
|
||||
let (coordinator_control, supervisor_control) = private_seqpacket_pair()?;
|
||||
let (supervisor_lifetime, coordinator_lifetime) =
|
||||
pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)?;
|
||||
let process = child_broker().acquire_with_stdio(
|
||||
|
|
@ -374,7 +423,19 @@ impl AwaitingAttachedRunsc {
|
|||
|staged| {
|
||||
coordinator
|
||||
.stage_runtime_supervisor(staged)
|
||||
.map_err(io::Error::other)
|
||||
.map_err(io::Error::other)?;
|
||||
if let Some(network) = network {
|
||||
network
|
||||
.begin_helper_intent(
|
||||
staged.generation_id(),
|
||||
launch.network_sidecar_id().ok_or_else(|| {
|
||||
io::Error::other("network sidecar identity is absent")
|
||||
})?,
|
||||
staged.identity(),
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|staged| {
|
||||
coordinator
|
||||
|
|
@ -387,9 +448,131 @@ impl AwaitingAttachedRunsc {
|
|||
control: Some(CoordinatorChannel::new(coordinator_control)),
|
||||
lifetime: Some(coordinator_lifetime),
|
||||
staged: None,
|
||||
helper_staged: None,
|
||||
network_evidence: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Receives, persists, and acknowledges the exact helper child acquisition.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses unexpected events, changed ownership, or a missing network launch.
|
||||
pub fn receive_helper_staged(
|
||||
&mut self,
|
||||
network: &NetworkCoordinator<'_>,
|
||||
) -> Result<StagedProcessOwnership, SandboxCoordinatorError> {
|
||||
if self.helper_staged.is_some() || self.network_evidence.is_some() {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"helper stage was already received",
|
||||
));
|
||||
}
|
||||
let control = self
|
||||
.control
|
||||
.as_ref()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor control channel is absent",
|
||||
))?;
|
||||
let staged = match control.receive()? {
|
||||
SupervisorEvent::HelperStaged(staged) => staged,
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
_ => {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not stage the helper first",
|
||||
));
|
||||
}
|
||||
};
|
||||
let runtime_supervisor = self
|
||||
.supervisor
|
||||
.as_ref()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor handle is absent",
|
||||
))?
|
||||
.identity();
|
||||
network.stage_helper(staged, runtime_supervisor)?;
|
||||
let acknowledgment = network.acknowledge_helper(staged)?;
|
||||
control.acknowledge_helper(acknowledgment)?;
|
||||
self.helper_staged = Some(staged);
|
||||
Ok(staged)
|
||||
}
|
||||
|
||||
/// Commits helper readiness, independently observes the namespace, and validates attestation.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses event ordering, namespace mutation, or any attestation mismatch.
|
||||
pub fn receive_network_readiness<BuildProbeRequest>(
|
||||
&mut self,
|
||||
network: &NetworkCoordinator<'_>,
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
build_probe_request: BuildProbeRequest,
|
||||
expected_reachability: &ReachabilityExpectation,
|
||||
) -> Result<NetworkReadinessEvidence, SandboxCoordinatorError>
|
||||
where
|
||||
BuildProbeRequest: FnOnce(NamespaceIdentity) -> NetworkProbeRequest,
|
||||
{
|
||||
let staged = self
|
||||
.helper_staged
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"helper ownership was not staged",
|
||||
))?;
|
||||
let control = self
|
||||
.control
|
||||
.as_ref()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor control channel is absent",
|
||||
))?;
|
||||
if control.receive()? != SupervisorEvent::HelperReady(staged) {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor helper readiness mismatched ownership",
|
||||
));
|
||||
}
|
||||
network.commit_helper(staged)?;
|
||||
let attestation = match control.receive()? {
|
||||
SupervisorEvent::NetworkAttestation(attestation) => attestation,
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
_ => {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not provide network attestation",
|
||||
));
|
||||
}
|
||||
};
|
||||
let namespace = network.begin_network_intent(
|
||||
attestation.generation_id,
|
||||
policy.digest(),
|
||||
attestation.runtime_supervisor,
|
||||
)?;
|
||||
network.validate_attestation(policy, &attestation)?;
|
||||
let probe_request = build_probe_request(namespace);
|
||||
if probe_request.domain_id != attestation.domain_id
|
||||
|| probe_request.generation_id != attestation.generation_id
|
||||
|| probe_request.enforcement_namespace != attestation.enforcement_namespace
|
||||
{
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"network probe request mismatches attestation",
|
||||
));
|
||||
}
|
||||
control.acknowledge_network_attestation(probe_request)?;
|
||||
let probe = match control.receive()? {
|
||||
SupervisorEvent::NetworkProbe(probe) => probe,
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
_ => {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not return the network probe",
|
||||
));
|
||||
}
|
||||
};
|
||||
let evidence =
|
||||
network.commit_readiness(policy, attestation, probe, expected_reachability)?;
|
||||
control.acknowledge_network_readiness()?;
|
||||
self.network_evidence = Some(evidence.clone());
|
||||
Ok(evidence)
|
||||
}
|
||||
|
||||
/// Receives and durably stages the exact RS child without releasing its barrier.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -403,6 +586,11 @@ impl AwaitingAttachedRunsc {
|
|||
"attached runsc stage was already received",
|
||||
));
|
||||
}
|
||||
if self.helper_staged.is_some() && self.network_evidence.is_none() {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"attached runsc cannot start before network attestation",
|
||||
));
|
||||
}
|
||||
let event = self
|
||||
.control
|
||||
.as_ref()
|
||||
|
|
@ -450,6 +638,7 @@ impl AwaitingAttachedRunsc {
|
|||
control: self.control.take(),
|
||||
lifetime: self.lifetime.take(),
|
||||
staged,
|
||||
network_evidence: self.network_evidence.take(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -474,6 +663,7 @@ pub struct AwaitingSandboxReadiness {
|
|||
control: Option<CoordinatorChannel>,
|
||||
lifetime: Option<std::os::fd::OwnedFd>,
|
||||
staged: StagedProcessOwnership,
|
||||
network_evidence: Option<NetworkReadinessEvidence>,
|
||||
}
|
||||
|
||||
impl AwaitingSandboxReadiness {
|
||||
|
|
@ -482,8 +672,39 @@ impl AwaitingSandboxReadiness {
|
|||
/// # Errors
|
||||
/// Refuses a mismatched readiness tuple, durable commit failure, or channel failure.
|
||||
pub fn commit(
|
||||
self,
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
) -> Result<CommittedSandboxRuntime, SandboxCoordinatorError> {
|
||||
if self.network_evidence.is_some() {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"networked sandbox requires a successful sandbox-view probe",
|
||||
));
|
||||
}
|
||||
self.commit_after_probe(coordinator, None)
|
||||
}
|
||||
|
||||
/// Commits the Sandbox only after the enforcement-namespace probe is durably accepted.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses missing attestation, failed/mutated probe evidence, or any runtime mismatch.
|
||||
pub fn commit_networked(
|
||||
mut self,
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
) -> Result<(CommittedSandboxRuntime, NetworkReadinessEvidence), SandboxCoordinatorError> {
|
||||
let evidence =
|
||||
self.network_evidence
|
||||
.take()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"network readiness evidence is absent",
|
||||
))?;
|
||||
let runtime = self.commit_after_probe(coordinator, Some(evidence.clone()))?;
|
||||
Ok((runtime, evidence))
|
||||
}
|
||||
|
||||
fn commit_after_probe(
|
||||
mut self,
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
network_evidence: Option<NetworkReadinessEvidence>,
|
||||
) -> Result<CommittedSandboxRuntime, SandboxCoordinatorError> {
|
||||
let control = self
|
||||
.control
|
||||
|
|
@ -520,6 +741,7 @@ impl AwaitingSandboxReadiness {
|
|||
"Runtime Supervisor control channel is absent",
|
||||
))?,
|
||||
lifetime: self.lifetime.take(),
|
||||
network_evidence,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -536,9 +758,28 @@ pub struct CommittedSandboxRuntime {
|
|||
supervisor: Option<RetainedProcess>,
|
||||
control: CoordinatorChannel,
|
||||
lifetime: Option<std::os::fd::OwnedFd>,
|
||||
network_evidence: Option<NetworkReadinessEvidence>,
|
||||
}
|
||||
|
||||
impl CommittedSandboxRuntime {
|
||||
#[must_use]
|
||||
pub const fn network_evidence(&self) -> Option<&NetworkReadinessEvidence> {
|
||||
self.network_evidence.as_ref()
|
||||
}
|
||||
|
||||
/// Withdraws live readiness after the RS reports exact helper-only loss.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses any event other than connectivity-helper loss.
|
||||
pub fn wait_for_helper_exit(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||
if self.control.receive()? != SupervisorEvent::HelperExited {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not report helper-only loss",
|
||||
));
|
||||
}
|
||||
self.network_evidence = None;
|
||||
Ok(())
|
||||
}
|
||||
#[must_use]
|
||||
pub fn runtime_supervisor_identity(&self) -> Option<PidfsProcessIdentityV1> {
|
||||
self.supervisor.as_ref().map(RetainedProcess::identity)
|
||||
|
|
|
|||
Loading…
Reference in a new issue