Implement supervised rootless network readiness

This commit is contained in:
ms2k 2026-07-15 23:26:37 +09:00
commit ba6a075839
5 changed files with 2917 additions and 54 deletions

View file

@ -3,5 +3,6 @@
pub mod control;
pub mod domain_store;
pub mod escalation;
pub mod network;
pub mod runtime;
pub mod state_anchor;

View 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;

View 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()
}

View file

@ -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)

View file

@ -1,7 +1,9 @@
use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream, ToSocketAddrs};
use std::os::linux::net::SocketAddrExt;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::net::UnixListener;
use std::os::unix::net::{SocketAddr, UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::thread;
@ -13,11 +15,13 @@ use lazaret_core::configuration::{
use lazaret_core::identity::{
BundleDigest, ContainerId, CoordinatorId, GenerationId, HostPath, NetworkSidecarId, SandboxId,
};
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation, derive_ipv4_policy};
use lazaret_core::records::{
Domain, DurableDecision, Generation, HostProcessOwnershipSlot, ParticipantRole,
Domain, DurableDecision, Generation, HostProcessOwnershipSlot, IpAddress, ParticipantRole,
ParticipantState, ProcessOwnershipCommitAck,
};
use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
use lazaret_host_coordinator::network::NetworkCoordinator;
use lazaret_host_coordinator::runtime::{
AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator,
};
@ -26,18 +30,22 @@ use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_linux::process::{
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker, current_process_identity,
};
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
use lazaret_runtime_supervisor::sandbox::{
NETWORK_NAMESPACE_PATH, RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
production_sandbox_layout,
};
use lazaret_runtime_supervisor::supervision::{
AttachedRunscSignal, RuntimeSupervisorLaunch, dispatch_private_runtime_role,
AttachedRunscSignal, RuntimeNetworkLaunch, RuntimeSupervisorLaunch,
dispatch_private_runtime_role,
};
use rustix::process::Signal;
use tempfile::TempDir;
const SKIP: u8 = 77;
const HOST_COORDINATOR_DRIVER: &str = "--runtime-composition-host-coordinator";
const SANDBOX_NETWORK_PROBE: &str = "--sandbox-network-probe";
const NETWORK_MATRIX: &str = "--network-matrix";
fn main() -> ExitCode {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
@ -50,6 +58,25 @@ fn main() -> ExitCode {
if arguments.first().and_then(|argument| argument.to_str()) == Some(HOST_COORDINATOR_DRIVER) {
return exit(run_host_coordinator_driver(&arguments[1..]));
}
if arguments.first().and_then(|argument| argument.to_str()) == Some(SANDBOX_NETWORK_PROBE) {
return exit(run_sandbox_network_probe(&arguments[1..]));
}
if arguments.first().and_then(|argument| argument.to_str()) == Some(NETWORK_MATRIX) {
return match run_network_matrix() {
Ok(()) => {
println!("PASS production network composition matrix");
ExitCode::SUCCESS
}
Err(MatrixError::Skip(reason)) => {
println!("SKIP network composition: {reason}");
ExitCode::from(SKIP)
}
Err(MatrixError::Failure(error)) => {
eprintln!("network composition failed: {error}");
ExitCode::FAILURE
}
};
}
match run_matrix() {
Ok(()) => {
println!("PASS foreground runtime composition and causal-loss matrix");
@ -88,6 +115,26 @@ impl From<std::io::Error> for MatrixError {
}
fn run_matrix() -> Result<(), MatrixError> {
require_matrix_prerequisites()?;
lost_acknowledgment_refuses_target_work()?;
normal_stop_and_stop_resume()?;
attached_runsc_loss_stops_guest()?;
runtime_supervisor_loss_stops_guest()?;
host_coordinator_loss_stops_guest()?;
run_network_cases()
}
fn run_network_matrix() -> Result<(), MatrixError> {
require_matrix_prerequisites()?;
run_network_cases()
}
fn run_network_cases() -> Result<(), MatrixError> {
failed_probe_blocks_sandbox_start()?;
networked_sandbox_view_isolated()
}
fn require_matrix_prerequisites() -> Result<(), MatrixError> {
let release = fs::read_to_string("/proc/sys/kernel/osrelease")?;
check_platform(&PlatformObservation {
operating_system: std::env::consts::OS,
@ -95,12 +142,7 @@ fn run_matrix() -> Result<(), MatrixError> {
kernel_release: release.trim(),
})
.map_err(|error| MatrixError::Skip(error.to_string()))?;
require_runtime_prerequisites()?;
lost_acknowledgment_refuses_target_work()?;
normal_stop_and_stop_resume()?;
attached_runsc_loss_stops_guest()?;
runtime_supervisor_loss_stops_guest()?;
host_coordinator_loss_stops_guest()
require_runtime_prerequisites()
}
fn require_runtime_prerequisites() -> Result<(), MatrixError> {
@ -115,6 +157,8 @@ fn require_runtime_prerequisites() -> Result<(), MatrixError> {
PathBuf::from("/bin/sh"),
PathBuf::from("/bin/sleep"),
PathBuf::from("/lib64/libc.so.6"),
PathBuf::from("/lib64/libgcc_s.so.1"),
PathBuf::from("/lib64/libm.so.6"),
PathBuf::from("/lib64/libtinfo.so.6"),
PathBuf::from("/lib64/ld-linux-x86-64.so.2"),
] {
@ -409,8 +453,8 @@ impl Fixture {
})
.map_err(failure)?;
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label());
stage_rootfs_and_control(&runtime_domain)?;
let container_id = ContainerId::new([6; 32]);
stage_rootfs_and_control(&runtime_domain, container_id)?;
let root_process = RootProcess::new(
"/bin/sh",
[
@ -502,7 +546,299 @@ impl Fixture {
}
}
fn stage_rootfs_and_control(runtime_domain: &Path) -> Result<(), MatrixError> {
struct NetworkFixture {
_tree: TempDir,
_host_loopback: TcpListener,
_host_abstract: UnixListener,
configuration: ResolvedDomainConfiguration,
store: HostCoordinatorDomainStore,
bundle: SandboxBundle,
policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
probe_result: PathBuf,
}
impl NetworkFixture {
fn new() -> Result<Self, MatrixError> {
let tree = tempfile::tempdir()?;
let root = tree.path();
let configuration_directory = root.join("configuration");
let workspace = root.join("workspace");
let runtime = root.join("runtime");
fs::create_dir(&configuration_directory)?;
fs::create_dir(&workspace)?;
fs::create_dir(&runtime)?;
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[profiles.default]\nimage = \"example.invalid/network-fixture:v1\"\npersistent_dirs = []\n",
root.join("data").display()
),
)?;
let configuration = load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "default".into(),
workspace: workspace.clone(),
home: root.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: production_sandbox_layout().map_err(failure)?,
})
.map_err(failure)?;
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label());
let container_id = ContainerId::new([16; 32]);
stage_rootfs_and_control(&runtime_domain, container_id)?;
let rootfs = runtime_domain.join("state-anchor/merged");
fs::copy(
std::env::current_exe()?,
rootfs.join("bin/lazaret-network-probe"),
)?;
fs::create_dir_all(rootfs.join("etc"))?;
fs::write(rootfs.join("etc/resolv.conf"), b"nameserver 10.0.2.3\n")?;
let host_loopback = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let host_loopback_port = host_loopback.local_addr()?.port();
let abstract_name = format!("lazaret-network-v3-{}", std::process::id()).into_bytes();
let host_abstract =
UnixListener::bind_addr(&SocketAddr::from_abstract_name(&abstract_name)?)?;
let root_process = RootProcess::new(
"/bin/lazaret-network-probe",
vec![
SANDBOX_NETWORK_PROBE.into(),
host_loopback_port.to_string(),
String::from_utf8(abstract_name.clone()).map_err(failure)?,
],
)
.map_err(failure)?;
let namespace =
lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))?;
let bundle = build_sandbox_bundle(SandboxBuildRequest {
configuration: &configuration,
container_id,
expected_network_namespace: namespace,
root_process,
terminal: None,
})
.map_err(failure)?;
let bundle_path = runtime_domain
.join("bundles")
.join(hex(container_id.as_bytes()));
fs::create_dir_all(&bundle_path)?;
fs::write(bundle_path.join("config.json"), bundle.specification_json())?;
fs::create_dir_all(runtime_domain.join("runsc"))?;
let domain = Domain {
domain_id: configuration.domain_id(),
profile_name: configuration.selected_profile().to_owned(),
canonical_workspace: configuration.workspace().canonical_label().clone(),
};
let persistent_domain = host_path(configuration.persistent_domain_root().canonical_label());
let lock = try_exclusive_flock(lock_file(&persistent_domain.join("domain.lock")))?;
let store =
HostCoordinatorDomainStore::open(lock, &persistent_domain.join("domain.redb"), &domain)
.map_err(failure)?;
store
.apply(&[DomainStoreMutation::PutGeneration(Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(7),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([13; 32]),
network_sidecar_id: NetworkSidecarId::new([14; 32]),
sandbox_id: SandboxId::new([15; 32]),
durable_decision: DurableDecision::None,
})])
.map_err(failure)?;
SandboxCoordinator::new(&store)
.begin_intent(
GenerationId::new(7),
container_id,
&runtime_domain.join("runsc"),
BundleDigest::new([17; 32]),
current_process_identity()?,
)
.map_err(failure)?;
let policy =
derive_ipv4_policy(&configuration, [IpAddress::V4([10, 0, 2, 3])]).map_err(failure)?;
Ok(Self {
_tree: tree,
_host_loopback: host_loopback,
_host_abstract: host_abstract,
configuration,
store,
bundle,
policy,
probe_result: workspace.join("network-probe"),
})
}
fn launch(&self) -> Result<CommittedSandboxRuntime, MatrixError> {
self.launch_with_permitted([1, 1, 1, 1], 443)
}
fn launch_with_permitted(
&self,
permitted_address: [u8; 4],
permitted_port: u16,
) -> Result<CommittedSandboxRuntime, MatrixError> {
let launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
ProcessOwnershipContext {
domain_id: self.configuration.domain_id(),
generation_id: GenerationId::new(7),
coordinator_id: CoordinatorId::new([13; 32]),
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
},
self.bundle.command(),
RuntimeNetworkLaunch {
policy: &self.policy,
configuration_fingerprint: self.configuration.configuration_fingerprint(),
network_sidecar_id: NetworkSidecarId::new([14; 32]),
},
)?;
let sandbox = SandboxCoordinator::new(&self.store);
let network = NetworkCoordinator::new(&self.store);
let mut pending = AwaitingAttachedRunsc::launch_networked(&sandbox, &network, &launch)
.map_err(failure)?;
pending.receive_helper_staged(&network).map_err(failure)?;
pending
.receive_network_readiness(
&network,
&self.policy,
|namespace| NetworkProbeRequest {
domain_id: self.configuration.domain_id(),
generation_id: GenerationId::new(7),
enforcement_namespace: namespace,
denied: vec![TcpProbeTarget {
address: IpAddress::V4([10, 1, 2, 3]),
port: 443,
}],
permitted: vec![TcpProbeTarget {
address: IpAddress::V4(permitted_address),
port: permitted_port,
}],
dns: vec![DnsProbeTarget {
resolver: IpAddress::V4([10, 0, 2, 3]),
query_name: "example.com".into(),
}],
},
&ReachabilityExpectation {
denied_destinations: vec![IpAddress::V4([10, 1, 2, 3])],
permitted_destinations: vec![IpAddress::V4(permitted_address)],
dns_queries: vec![DnsQueryExpectation {
resolver: IpAddress::V4([10, 0, 2, 3]),
query_name: "example.com".into(),
}],
},
)
.map_err(failure)?;
let staged = pending.receive_staged(&sandbox).map_err(failure)?;
let (runtime, _) = pending
.acknowledge(staged)
.map_err(failure)?
.commit_networked(&sandbox)
.map_err(failure)?;
Ok(runtime)
}
}
fn failed_probe_blocks_sandbox_start() -> Result<(), MatrixError> {
let fixture = NetworkFixture::new()?;
match fixture.launch_with_permitted([203, 0, 113, 1], 9) {
Ok(runtime) => {
runtime
.stop(&SandboxCoordinator::new(&fixture.store))
.map_err(failure)?;
Err(failure("failed physical probe started the sandbox"))
}
Err(_) => {
let sandbox = sandbox_participant(&fixture.store);
if sandbox.state() != ParticipantState::Intent || fixture.probe_result.exists() {
Err(failure(
"failed physical probe advanced Sandbox state or guest work",
))
} else {
Ok(())
}
}
}
}
fn networked_sandbox_view_isolated() -> Result<(), MatrixError> {
let fixture = NetworkFixture::new()?;
let runtime = fixture.launch()?;
for _ in 0..6000 {
if fixture.probe_result.is_file() {
break;
}
thread::sleep(Duration::from_millis(5));
}
let result = fs::read_to_string(&fixture.probe_result)?;
for required in [
"denied=true",
"public=true",
"dns=true",
"host_loopback=false",
"abstract_socket=false",
] {
if !result.lines().any(|line| line == required) {
return Err(failure(format!(
"guest network assertion missing: {required}"
)));
}
}
runtime
.stop(&SandboxCoordinator::new(&fixture.store))
.map_err(failure)
}
fn run_sandbox_network_probe(arguments: &[std::ffi::OsString]) -> std::io::Result<()> {
let host_port = arguments
.first()
.and_then(|value| value.to_str())
.and_then(|value| value.parse::<u16>().ok())
.filter(|port| *port != 0)
.ok_or_else(|| std::io::Error::other("sandbox probe host port is invalid"))?;
let abstract_name = arguments
.get(1)
.and_then(|value| value.to_str())
.ok_or_else(|| std::io::Error::other("sandbox probe abstract name is invalid"))?;
let timeout = Duration::from_secs(3);
let denied = TcpStream::connect_timeout(
&SocketAddrV4::new(Ipv4Addr::new(10, 1, 2, 3), 443).into(),
timeout,
)
.is_err();
let public = TcpStream::connect_timeout(
&SocketAddrV4::new(Ipv4Addr::new(1, 1, 1, 1), 443).into(),
timeout,
)
.is_ok();
let dns = ("example.com", 443)
.to_socket_addrs()
.is_ok_and(|mut addresses| addresses.next().is_some());
let host_loopback = TcpStream::connect_timeout(
&SocketAddrV4::new(Ipv4Addr::new(10, 0, 2, 2), host_port).into(),
timeout,
)
.is_ok();
let abstract_socket =
UnixStream::connect_addr(&SocketAddr::from_abstract_name(abstract_name.as_bytes())?)
.is_ok();
fs::write(
"/workspace/.network-probe",
format!(
"denied={denied}\npublic={public}\ndns={dns}\nhost_loopback={host_loopback}\nabstract_socket={abstract_socket}\n"
),
)?;
fs::rename("/workspace/.network-probe", "/workspace/network-probe")?;
loop {
thread::sleep(Duration::from_secs(1));
}
}
fn stage_rootfs_and_control(
runtime_domain: &Path,
container_id: ContainerId,
) -> Result<(), MatrixError> {
let rootfs = runtime_domain.join("state-anchor/merged");
for directory in ["bin", "lib64"] {
fs::create_dir_all(rootfs.join(directory))?;
@ -511,12 +847,16 @@ fn stage_rootfs_and_control(runtime_domain: &Path) -> Result<(), MatrixError> {
("/bin/sh", "bin/sh"),
("/bin/sleep", "bin/sleep"),
("/lib64/libc.so.6", "lib64/libc.so.6"),
("/lib64/libgcc_s.so.1", "lib64/libgcc_s.so.1"),
("/lib64/libm.so.6", "lib64/libm.so.6"),
("/lib64/libtinfo.so.6", "lib64/libtinfo.so.6"),
("/lib64/ld-linux-x86-64.so.2", "lib64/ld-linux-x86-64.so.2"),
] {
fs::copy(source, rootfs.join(destination))?;
}
let control = runtime_domain.join("sandbox-control").join(hex(&[6; 32]));
let control = runtime_domain
.join("sandbox-control")
.join(hex(container_id.as_bytes()));
for directory in ["credentials", "mailbox/requests", "mailbox/results", "bin"] {
fs::create_dir_all(control.join(directory))?;
}