Replace external reachability with an owned canary
This commit is contained in:
parent
0f6825c3b9
commit
31e9ab0f2d
22 changed files with 1409 additions and 1972 deletions
|
|
@ -5,14 +5,13 @@ use std::sync::Mutex;
|
|||
|
||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
|
||||
use lazaret_core::records::Session;
|
||||
use lazaret_core::records::{
|
||||
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
|
||||
ParticipantState, PreparedArtifact,
|
||||
};
|
||||
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||
use lazaret_state_anchor::StateAnchorLayout;
|
||||
|
|
@ -70,8 +69,6 @@ pub struct BootstrapPlan<'a> {
|
|||
pub runtime_launch: RuntimeSupervisorLaunch,
|
||||
pub sandbox_bundle: &'a SandboxBundle,
|
||||
pub network_policy: &'a CanonicalDerivedNetworkPolicy,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
pub reachability: &'a ReachabilityExpectation,
|
||||
pub container_id: ContainerId,
|
||||
pub runtime_root: PathBuf,
|
||||
pub bundle_digest: BundleDigest,
|
||||
|
|
@ -98,6 +95,7 @@ pub struct CoordinatorLifecycleAdapter<'a> {
|
|||
escalation: Option<EscalationService<'a>>,
|
||||
escalation_handle: Option<EscalationServiceHandle>,
|
||||
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
||||
awaiting_sandbox: Option<crate::runtime::AwaitingSandboxReadiness>,
|
||||
runtime: Option<CommittedSandboxRuntime>,
|
||||
state_anchor_mount: Option<MountIdentity>,
|
||||
git_routing_mount: Option<MountIdentity>,
|
||||
|
|
@ -197,6 +195,7 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
escalation: None,
|
||||
escalation_handle: None,
|
||||
awaiting_runtime: None,
|
||||
awaiting_sandbox: None,
|
||||
runtime: None,
|
||||
state_anchor_mount: None,
|
||||
git_routing_mount: None,
|
||||
|
|
@ -330,12 +329,16 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
fn start_connectivity_helper(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
self.awaiting_runtime
|
||||
let awaiting = self
|
||||
.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
|
||||
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?;
|
||||
awaiting
|
||||
.receive_helper_staged(&network)
|
||||
.map(|_| ())
|
||||
.map_err(|error| failure(format!("starting connectivity helper: {error}")))
|
||||
.map_err(|error| failure(format!("starting connectivity helper: {error}")))?;
|
||||
awaiting
|
||||
.receive_network_attestation(&network, self.plan.network_policy)
|
||||
.map_err(|error| failure(format!("attesting prepared network: {error}")))
|
||||
}
|
||||
|
||||
fn start_sidecar(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
|
|
@ -349,21 +352,16 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
fn mark_network_ready(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
let mut probe = self.plan.network_probe.clone();
|
||||
self.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
|
||||
.receive_network_readiness(
|
||||
&network,
|
||||
self.plan.network_policy,
|
||||
|namespace| {
|
||||
probe.enforcement_namespace = namespace;
|
||||
probe
|
||||
},
|
||||
self.plan.reachability,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|error| failure(format!("marking network ready: {error}")))
|
||||
let sandbox = SandboxCoordinator::new(&store);
|
||||
let pending = self
|
||||
.awaiting_sandbox
|
||||
.take()
|
||||
.ok_or_else(|| LifecycleServiceError("Sandbox readiness is absent".into()))?;
|
||||
let (runtime, _) = pending
|
||||
.commit_networked(&sandbox, &network, self.plan.network_policy)
|
||||
.map_err(|error| failure(format!("marking network ready: {error}")))?;
|
||||
self.runtime = Some(runtime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_sandbox(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
|
|
@ -381,12 +379,10 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
.sandbox_bundle
|
||||
.capture_handoff_authority(self.plan.configuration)
|
||||
.map_err(|error| failure(format!("capturing sandbox handoff: {error}")))?;
|
||||
let (runtime, _) = pending
|
||||
let awaiting = pending
|
||||
.acknowledge(staged, &handoff)
|
||||
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?
|
||||
.commit_networked(&sandbox)
|
||||
.map_err(|error| failure(format!("committing sandbox readiness: {error}")))?;
|
||||
self.runtime = Some(runtime);
|
||||
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?;
|
||||
self.awaiting_sandbox = Some(awaiting);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
|
|||
Arc::clone(&store),
|
||||
InstalledProductPaths {
|
||||
escalation_client: &installed.join("lazaret-escalation-client"),
|
||||
sandbox_init: &executable,
|
||||
sandbox_init: &installed.join("lazaret-sandbox-init"),
|
||||
},
|
||||
))
|
||||
.map_err(failure)?;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ use std::sync::{Arc, Mutex};
|
|||
|
||||
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
|
||||
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
|
||||
use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy};
|
||||
use lazaret_core::network::derive_ipv4_policy;
|
||||
use lazaret_core::records::{
|
||||
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
|
||||
Generation, GenerationIdentity, HostProcessOwnershipSlot, PreparedArtifact, Session,
|
||||
};
|
||||
use lazaret_ignored_workspace::git_common_directory;
|
||||
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
|
||||
|
|
@ -21,7 +21,6 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
|
||||
build_sandbox_bundle_with_workspace,
|
||||
|
|
@ -79,9 +78,6 @@ pub struct ColdStartInputs<'a> {
|
|||
pub prepared_artifact_authority: &'a File,
|
||||
pub prepared_artifact_label: &'a Path,
|
||||
pub root_process: RootProcess,
|
||||
pub resolver_addresses: Vec<IpAddress>,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
pub reachability: ReachabilityExpectation,
|
||||
pub container_id: ContainerId,
|
||||
pub invoking_uid: u32,
|
||||
pub escalation_client: &'a File,
|
||||
|
|
@ -142,8 +138,6 @@ pub struct ColdStart<'a> {
|
|||
runtime_launch: Option<RuntimeSupervisorLaunch>,
|
||||
sandbox_bundle: SandboxBundle,
|
||||
network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
|
||||
network_probe: NetworkProbeRequest,
|
||||
reachability: ReachabilityExpectation,
|
||||
container_id: ContainerId,
|
||||
runtime_root: PathBuf,
|
||||
bundle_digest: BundleDigest,
|
||||
|
|
@ -204,7 +198,7 @@ impl<'a> ColdStart<'a> {
|
|||
staging
|
||||
.write_bundle(sandbox_bundle.specification_json())
|
||||
.map_err(|error| failure(format!("bundle staging: {error}")))?;
|
||||
let network_policy = derive_ipv4_policy(inputs.configuration, inputs.resolver_addresses)
|
||||
let network_policy = derive_ipv4_policy(inputs.configuration)
|
||||
.map_err(|error| failure(format!("network policy: {error}")))?;
|
||||
let ownership = ProcessOwnershipContext {
|
||||
domain_id: inputs.generation.domain_id,
|
||||
|
|
@ -263,8 +257,6 @@ impl<'a> ColdStart<'a> {
|
|||
runtime_launch: Some(runtime_launch),
|
||||
sandbox_bundle,
|
||||
network_policy,
|
||||
network_probe: inputs.network_probe,
|
||||
reachability: inputs.reachability,
|
||||
container_id: inputs.container_id,
|
||||
runtime_root: runtime_domain.join("runsc"),
|
||||
bundle_digest,
|
||||
|
|
@ -366,6 +358,7 @@ impl<'a> ColdStart<'a> {
|
|||
self.container_id,
|
||||
current_process_identity().map_err(failure)?,
|
||||
self.sandbox_bundle.command(),
|
||||
self.configuration.ephemeral_environment().clone(),
|
||||
self.runtime_root.clone(),
|
||||
)
|
||||
.map_err(failure)?;
|
||||
|
|
@ -383,8 +376,6 @@ impl<'a> ColdStart<'a> {
|
|||
.ok_or_else(|| ColdStartError("cold start was already consumed".into()))?,
|
||||
sandbox_bundle: &self.sandbox_bundle,
|
||||
network_policy: &self.network_policy,
|
||||
network_probe: self.network_probe.clone(),
|
||||
reachability: &self.reachability,
|
||||
container_id: self.container_id,
|
||||
runtime_root: self.runtime_root.clone(),
|
||||
bundle_digest: self.bundle_digest,
|
||||
|
|
@ -393,6 +384,7 @@ impl<'a> ColdStart<'a> {
|
|||
RunscNonInteractiveExecutor::from_run_command(
|
||||
self.sandbox_bundle.command(),
|
||||
self.container_id,
|
||||
self.configuration.ephemeral_environment().clone(),
|
||||
)
|
||||
.map_err(failure)?,
|
||||
),
|
||||
|
|
@ -414,9 +406,6 @@ fn require_inputs(inputs: &ColdStartInputs<'_>) -> Result<(), ColdStartError> {
|
|||
if inputs.generation.domain_id != inputs.configuration.domain_id()
|
||||
|| inputs.generation.configuration_fingerprint
|
||||
!= inputs.configuration.configuration_fingerprint()
|
||||
|| inputs.network_probe.domain_id != inputs.generation.domain_id
|
||||
|| inputs.network_probe.generation_id != inputs.generation.generation_id
|
||||
|| inputs.resolver_addresses.is_empty()
|
||||
|| inputs.invoking_uid != rustix::process::getuid().as_raw()
|
||||
{
|
||||
return Err(ColdStartError("cold-start inputs disagree".into()));
|
||||
|
|
|
|||
|
|
@ -130,6 +130,12 @@ impl RuntimeStaging {
|
|||
invoking_uid,
|
||||
true,
|
||||
)?);
|
||||
drop(open_or_create_private_directory(
|
||||
&self.control,
|
||||
"readiness",
|
||||
invoking_uid,
|
||||
true,
|
||||
)?);
|
||||
let mailbox =
|
||||
open_or_create_private_directory(&self.control, "mailbox", invoking_uid, true)?;
|
||||
drop(open_or_create_private_directory(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use ed25519_dalek::SigningKey;
|
|||
pub use lazaret_control_client::{CONTROL_ALPN, CONTROL_SERVER_NAME, PendingClientEnrollment};
|
||||
use lazaret_control_client::{ControlClient, ControlClientError};
|
||||
use lazaret_core::codec::{CodecError, decode, encode};
|
||||
use lazaret_core::configuration::ConfinedPathPlan;
|
||||
use lazaret_core::configuration::{ConfinedPathPlan, EnvironmentValue};
|
||||
use lazaret_core::identity::{
|
||||
CertificateDigest, ClientId, ContainerId, DeviceId, HostPath, InodeId, MountIdentity,
|
||||
SessionAnchorId, SessionId, TrustManifestDigest,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use super::{
|
||||
Arc, AsFd, CONTROL_SOCKET_NAME, ContainerId, ControlError, File, FileIdentity,
|
||||
InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionRequest, MetadataExt, Mode,
|
||||
NonInteractiveSessionMessage, NonInteractiveSessionRequest, OFlags, OpenOptions, OsStr,
|
||||
OsString, OsStringExt, PUBLICATION_IDENTITY_NAME, Path, PathBuf, ProcessExit,
|
||||
ProtocolPublicationIdentity, Read, RunscCommand, STAGED_CONTROL_SOCKET_NAME,
|
||||
Arc, AsFd, BTreeMap, CONTROL_SOCKET_NAME, ContainerId, ControlError, EnvironmentValue, File,
|
||||
FileIdentity, InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionRequest,
|
||||
MetadataExt, Mode, NonInteractiveSessionMessage, NonInteractiveSessionRequest, OFlags,
|
||||
OpenOptions, OsStr, OsString, OsStringExt, PUBLICATION_IDENTITY_NAME, Path, PathBuf,
|
||||
ProcessExit, ProtocolPublicationIdentity, Read, RunscCommand, STAGED_CONTROL_SOCKET_NAME,
|
||||
STAGED_PUBLICATION_IDENTITY_NAME, ServerConfig, SessionExit, SessionId, SessionRegistry,
|
||||
TargetCommand, TrustAnchor, Write, child_broker, fchmod, fmt, io, openat,
|
||||
remove_if_identity_matches,
|
||||
|
|
@ -221,6 +221,7 @@ impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
|
|||
pub struct RunscNonInteractiveExecutor {
|
||||
pub(super) program: OsString,
|
||||
pub(super) global_arguments: Vec<OsString>,
|
||||
pub(super) environment: Vec<(String, OsString)>,
|
||||
pub(super) container: OsString,
|
||||
pub(super) runner: Arc<dyn NonInteractiveCommandRunner>,
|
||||
}
|
||||
|
|
@ -268,6 +269,7 @@ impl RunscNonInteractiveExecutor {
|
|||
pub fn from_run_command(
|
||||
command: &RunscCommand,
|
||||
container_id: ContainerId,
|
||||
environment: BTreeMap<String, EnvironmentValue>,
|
||||
) -> Result<Self, ControlError> {
|
||||
if command.program() != OsStr::new("runsc") {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
|
|
@ -291,6 +293,10 @@ impl RunscNonInteractiveExecutor {
|
|||
Ok(Self {
|
||||
program: command.program().to_owned(),
|
||||
global_arguments: command.arguments()[..run].to_vec(),
|
||||
environment: environment
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.expose().to_owned()))
|
||||
.collect(),
|
||||
container,
|
||||
runner: Arc::new(ChildBrokerCommandRunner),
|
||||
})
|
||||
|
|
@ -307,8 +313,19 @@ impl NonInteractiveSessionExecutor for RunscNonInteractiveExecutor {
|
|||
for argument in &self.global_arguments {
|
||||
target = target.argument(argument.clone());
|
||||
}
|
||||
target = target.argument("exec");
|
||||
if !self.environment.iter().any(|(name, _)| name == "PATH") {
|
||||
target = target
|
||||
.argument("-env")
|
||||
.argument(format!("PATH={}", crate::sandbox_init::DEFAULT_AGENT_PATH));
|
||||
}
|
||||
for (name, value) in &self.environment {
|
||||
let mut assignment = OsString::from(name);
|
||||
assignment.push("=");
|
||||
assignment.push(value);
|
||||
target = target.argument("-env").argument(assignment);
|
||||
}
|
||||
target = target
|
||||
.argument("exec")
|
||||
.argument("-env")
|
||||
.argument(format!(
|
||||
"LAZARET_SESSION_ID={}",
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ impl NonInteractiveCommandRunner for RecordingRunner {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
|
||||
fn runsc_exec_places_profile_environment_before_session_binding_and_command() {
|
||||
let runner = Arc::new(RecordingRunner {
|
||||
observed: Mutex::new(None),
|
||||
exit: ProcessExit {
|
||||
|
|
@ -105,6 +105,7 @@ fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
|
|||
let executor = RunscNonInteractiveExecutor {
|
||||
program: "runsc".into(),
|
||||
global_arguments: vec!["--root=/runtime".into()],
|
||||
environment: vec![("PROFILE_SENTINEL".into(), "profile-value".into())],
|
||||
container: "container-id".into(),
|
||||
runner: runner.clone(),
|
||||
};
|
||||
|
|
@ -129,6 +130,10 @@ fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
|
|||
OsString::from("--root=/runtime"),
|
||||
OsString::from("exec"),
|
||||
OsString::from("-env"),
|
||||
OsString::from(format!("PATH={}", crate::sandbox_init::DEFAULT_AGENT_PATH)),
|
||||
OsString::from("-env"),
|
||||
OsString::from("PROFILE_SENTINEL=profile-value"),
|
||||
OsString::from("-env"),
|
||||
OsString::from(format!(
|
||||
"LAZARET_SESSION_ID={}",
|
||||
hex_lower(session_id.as_bytes())
|
||||
|
|
|
|||
|
|
@ -180,12 +180,12 @@ where
|
|||
checkpoint!();
|
||||
self.start_connectivity_helper()?;
|
||||
checkpoint!();
|
||||
self.mark_network_ready()?;
|
||||
checkpoint!();
|
||||
self.arm_runtime_causal_edge()?;
|
||||
checkpoint!();
|
||||
self.start_sandbox()?;
|
||||
checkpoint!();
|
||||
self.mark_network_ready()?;
|
||||
checkpoint!();
|
||||
withdraw_waiter_admission(self).map_err(LifecycleError::Adapter)?;
|
||||
if !owner_live() {
|
||||
self.disconnect_session(session_id)?;
|
||||
|
|
@ -419,9 +419,7 @@ where
|
|||
pub fn mark_network_ready(&mut self) -> Result<(), LifecycleError<A::Error>> {
|
||||
self.startup_action(
|
||||
LifecycleAction::MarkNetworkReady,
|
||||
self.state
|
||||
.progress
|
||||
.has(RuntimeMilestone::ConnectivityHelper)
|
||||
self.state.progress.has(RuntimeMilestone::Sandbox)
|
||||
&& !self.state.progress.has(RuntimeMilestone::NetworkReady),
|
||||
|p| p.mark(RuntimeMilestone::NetworkReady),
|
||||
)
|
||||
|
|
@ -432,17 +430,22 @@ where
|
|||
pub fn arm_runtime_causal_edge(&mut self) -> Result<(), LifecycleError<A::Error>> {
|
||||
self.startup_action(
|
||||
LifecycleAction::ArmRuntimeCausalEdge,
|
||||
self.state.progress.has(RuntimeMilestone::NetworkReady)
|
||||
self.state
|
||||
.progress
|
||||
.has(RuntimeMilestone::ConnectivityHelper)
|
||||
&& !self.state.progress.has(RuntimeMilestone::CausalEdge),
|
||||
|p| p.mark(RuntimeMilestone::CausalEdge),
|
||||
)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses launch before network readiness and the causal edge are established.
|
||||
/// Refuses launch before helper attestation and the causal edge are established.
|
||||
pub fn start_sandbox(&mut self) -> Result<(), LifecycleError<A::Error>> {
|
||||
let allowed = self.state.progress.has(RuntimeMilestone::CausalEdge)
|
||||
&& self.state.progress.has(RuntimeMilestone::NetworkReady)
|
||||
&& self
|
||||
.state
|
||||
.progress
|
||||
.has(RuntimeMilestone::ConnectivityHelper)
|
||||
&& !self.state.progress.has(RuntimeMilestone::Sandbox);
|
||||
Self::require(
|
||||
LifecycleAction::StartSandbox,
|
||||
|
|
@ -692,6 +695,7 @@ where
|
|||
LifecycleAction::StopSandbox,
|
||||
self.state.progress.has(RuntimeMilestone::Sandbox),
|
||||
|p| {
|
||||
p.clear(RuntimeMilestone::NetworkReady);
|
||||
p.clear(RuntimeMilestone::Sandbox);
|
||||
p.clear(RuntimeMilestone::CausalEdge);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ pub(super) enum RuntimeMilestone {
|
|||
StateAnchor,
|
||||
Sidecar,
|
||||
ConnectivityHelper,
|
||||
NetworkReady,
|
||||
CausalEdge,
|
||||
Sandbox,
|
||||
NetworkReady,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
|
|
@ -51,9 +51,9 @@ impl RuntimeProgress {
|
|||
RuntimeMilestone::StateAnchor,
|
||||
RuntimeMilestone::Sidecar,
|
||||
RuntimeMilestone::ConnectivityHelper,
|
||||
RuntimeMilestone::NetworkReady,
|
||||
RuntimeMilestone::CausalEdge,
|
||||
RuntimeMilestone::Sandbox,
|
||||
RuntimeMilestone::NetworkReady,
|
||||
];
|
||||
let completed = ORDER
|
||||
.iter()
|
||||
|
|
@ -92,7 +92,7 @@ impl LifecycleAggregate {
|
|||
if !self.progress.validate_prefix() {
|
||||
return Err("runtime milestones are not an ordered prefix");
|
||||
}
|
||||
if self.phase == DomainPhase::Ready && !self.progress.has(RuntimeMilestone::Sandbox) {
|
||||
if self.phase == DomainPhase::Ready && !self.progress.has(RuntimeMilestone::NetworkReady) {
|
||||
return Err("ready Domain has incomplete runtime progress");
|
||||
}
|
||||
if self.phase == DomainPhase::Starting {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ use lazaret_core::identity::{
|
|||
DerivedPolicyDigest, GenerationId, NamespaceIdentity, NetworkSidecarId, PidfsProcessIdentityV1,
|
||||
};
|
||||
use lazaret_core::network::{
|
||||
CanonicalDerivedNetworkPolicy, ReachabilityExpectation, ReadinessExpectation,
|
||||
accept_network_readiness, validate_network_attestation,
|
||||
CanonicalDerivedNetworkPolicy, ReadinessExpectation, accept_network_readiness,
|
||||
validate_network_attestation,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
HelperOwnership, HostProcessOwnershipSlot, NetworkOwnership, NetworkReachabilityProbe,
|
||||
HelperOwnership, HostProcessOwnershipSlot, NetworkCanaryObservation, NetworkOwnership,
|
||||
NetworkReadinessAttestation, NetworkReadinessEvidence, Participant, ParticipantOwnership,
|
||||
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
|
||||
StagedProcessOwnership,
|
||||
|
|
@ -289,8 +289,7 @@ impl<'store> NetworkCoordinator<'store> {
|
|||
&self,
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
attestation: NetworkReadinessAttestation,
|
||||
probe: NetworkReachabilityProbe,
|
||||
expected_reachability: &ReachabilityExpectation,
|
||||
canary: NetworkCanaryObservation,
|
||||
) -> Result<NetworkReadinessEvidence, NetworkCoordinatorError> {
|
||||
let generation_id = attestation.generation_id;
|
||||
let snapshot = self.domain_store.snapshot()?;
|
||||
|
|
@ -350,14 +349,8 @@ impl<'store> NetworkCoordinator<'store> {
|
|||
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()))?;
|
||||
let evidence = accept_network_readiness(policy, &expectation, attestation, canary)
|
||||
.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(
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ use lazaret_core::identity::{
|
|||
CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId, HostPath, NamespaceIdentity,
|
||||
NetworkSidecarId, PidfsProcessIdentityV1, SandboxId,
|
||||
};
|
||||
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation, derive_policy};
|
||||
use lazaret_core::network::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,
|
||||
DerivedNetworkPolicyFields, Domain, DurableDecision, Generation, HostProcessOwnershipSlot,
|
||||
IpAddress, IpNetwork, NetworkCanaryObservation, NetworkObservation,
|
||||
NetworkReadinessAttestation, NetworkReadinessAttestationFields, ParticipantOwnership,
|
||||
ParticipantRole, ParticipantState, PolicyAction, PolicyDelivery, PrefixRule, Resolver,
|
||||
StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
|
||||
|
|
@ -93,35 +92,15 @@ impl Fixture {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn failed_probe_leaves_network_intent_uncommitted() {
|
||||
fn failed_canary_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(),
|
||||
);
|
||||
let mut canary = canary(&fixture.generation, namespace);
|
||||
canary.denied_endpoint_unreachable = false;
|
||||
let result =
|
||||
NetworkCoordinator::new(&fixture.store).commit_readiness(&policy, attestation, canary);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NetworkCoordinatorError::InvalidEvidence(_))
|
||||
|
|
@ -161,27 +140,9 @@ fn committed_network_roles_release_together_only_after_absence() {
|
|||
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: true,
|
||||
}],
|
||||
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 coordinator = NetworkCoordinator::new(&fixture.store);
|
||||
coordinator
|
||||
.commit_readiness(&policy, attestation, probe, &reachability())
|
||||
.commit_readiness(&policy, attestation, canary(&fixture.generation, namespace))
|
||||
.unwrap();
|
||||
assert!(
|
||||
coordinator
|
||||
|
|
@ -275,14 +236,28 @@ fn attestation(
|
|||
})
|
||||
}
|
||||
|
||||
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 canary(generation: &Generation, namespace: NamespaceIdentity) -> NetworkCanaryObservation {
|
||||
NetworkCanaryObservation {
|
||||
domain_id: generation.domain_id,
|
||||
generation_id: generation.generation_id,
|
||||
enforcement_namespace: namespace,
|
||||
linux_policy_matches: true,
|
||||
endpoints_authenticated_before_policy: true,
|
||||
allowed_endpoint_authenticated: true,
|
||||
denied_endpoint_unreachable: true,
|
||||
imported_interface_matches: true,
|
||||
tap_present: true,
|
||||
ipv6_absent: true,
|
||||
network_capabilities_empty: true,
|
||||
route_mutation_refused: true,
|
||||
cleanup_shape_matched: true,
|
||||
responder_closed: true,
|
||||
peer_namespace_closed: true,
|
||||
host_interface_deleted: true,
|
||||
allowed_endpoint_unreachable_after_cleanup: true,
|
||||
tap_present_after_cleanup: true,
|
||||
ipv6_unchanged_after_cleanup: true,
|
||||
network_admin_dropped: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -904,7 +904,8 @@ fn test_configuration(root: &Path) -> ResolvedDomainConfiguration {
|
|||
fs::write(
|
||||
&configuration_path,
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"
|
||||
",
|
||||
root.join("data").display()
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,17 +9,16 @@ use lazaret_core::identity::{
|
|||
BundleDigest, ContainerId, GenerationId, HostPath, MountIdentity, NamespaceIdentity,
|
||||
PidfsProcessIdentityV1, PreparedArtifactId,
|
||||
};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
|
||||
use lazaret_core::records::{
|
||||
HostProcessOwnershipSlot, NetworkReadinessEvidence, Participant, ParticipantOwnership,
|
||||
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
|
||||
SandboxOwnership, StagedProcessOwnership,
|
||||
HostProcessOwnershipSlot, NetworkReadinessAttestation, NetworkReadinessEvidence, Participant,
|
||||
ParticipantOwnership, ParticipantRole, ParticipantState, PersistedHostProcessOwnership,
|
||||
ProcessOwnershipCommitAck, SandboxOwnership, StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::ipc::private_seqpacket_pair;
|
||||
use lazaret_linux::process::{
|
||||
AcknowledgedChildAcquisition, RetainedProcess, child_broker, current_process_identity,
|
||||
};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::SandboxHandoffAuthority;
|
||||
use lazaret_runtime_supervisor::supervision::{
|
||||
AttachedRunscSignal, CoordinatorChannel, RuntimeSupervisorLaunch, SupervisorEvent,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
use super::{
|
||||
AcknowledgedChildAcquisition, CanonicalDerivedNetworkPolicy, CommittedSandboxRuntime,
|
||||
CoordinatorChannel, GitRoutingCoordinator, MountIdentity, NamespaceIdentity,
|
||||
NetworkCoordinator, NetworkProbeRequest, NetworkReadinessEvidence, PidfsProcessIdentityV1,
|
||||
PipeFlags, PreparedArtifactId, ProcessOwnershipCommitAck, ReachabilityExpectation,
|
||||
RetainedProcess, RuntimeSupervisorLaunch, SandboxCoordinator, SandboxCoordinatorError,
|
||||
SandboxHandoffAuthority, StagedProcessOwnership, StateAnchorCoordinator, SupervisorEvent,
|
||||
child_broker, cleanup_supervisor, current_process_identity, io, pipe_with,
|
||||
private_seqpacket_pair,
|
||||
CoordinatorChannel, GitRoutingCoordinator, MountIdentity, NetworkCoordinator,
|
||||
NetworkReadinessAttestation, NetworkReadinessEvidence, PidfsProcessIdentityV1, PipeFlags,
|
||||
PreparedArtifactId, ProcessOwnershipCommitAck, RetainedProcess, RuntimeSupervisorLaunch,
|
||||
SandboxCoordinator, SandboxCoordinatorError, SandboxHandoffAuthority, StagedProcessOwnership,
|
||||
StateAnchorCoordinator, SupervisorEvent, child_broker, cleanup_supervisor,
|
||||
current_process_identity, io, pipe_with, private_seqpacket_pair,
|
||||
};
|
||||
|
||||
pub struct AwaitingAttachedRunsc {
|
||||
|
|
@ -18,6 +17,7 @@ pub struct AwaitingAttachedRunsc {
|
|||
git_routing_required: bool,
|
||||
git_routing_mount: Option<MountIdentity>,
|
||||
network_evidence: Option<NetworkReadinessEvidence>,
|
||||
network_attestation: Option<NetworkReadinessAttestation>,
|
||||
runsc_command: lazaret_runtime_supervisor::sandbox::RunscCommand,
|
||||
}
|
||||
|
||||
|
|
@ -263,6 +263,7 @@ impl AwaitingAttachedRunsc {
|
|||
git_routing_required: launch.has_git_routing(),
|
||||
git_routing_mount: None,
|
||||
network_evidence: None,
|
||||
network_attestation: None,
|
||||
runsc_command: launch.runsc_command().clone(),
|
||||
})
|
||||
}
|
||||
|
|
@ -315,16 +316,11 @@ impl AwaitingAttachedRunsc {
|
|||
///
|
||||
/// # Errors
|
||||
/// Refuses event ordering, namespace mutation, or any attestation mismatch.
|
||||
pub fn receive_network_readiness<BuildProbeRequest>(
|
||||
pub fn receive_network_attestation(
|
||||
&mut self,
|
||||
network: &NetworkCoordinator<'_>,
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
build_probe_request: BuildProbeRequest,
|
||||
expected_reachability: &ReachabilityExpectation,
|
||||
) -> Result<NetworkReadinessEvidence, SandboxCoordinatorError>
|
||||
where
|
||||
BuildProbeRequest: FnOnce(NamespaceIdentity) -> NetworkProbeRequest,
|
||||
{
|
||||
) -> Result<(), SandboxCoordinatorError> {
|
||||
let staged = self
|
||||
.helper_staged
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
|
|
@ -359,32 +355,14 @@ impl AwaitingAttachedRunsc {
|
|||
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
|
||||
{
|
||||
if namespace != attestation.enforcement_namespace {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"network probe request mismatches attestation",
|
||||
"network namespace changed during 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)
|
||||
control.acknowledge_network_attestation()?;
|
||||
self.network_attestation = Some(attestation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receives and durably stages the exact RS child without releasing its barrier.
|
||||
|
|
@ -400,7 +378,7 @@ impl AwaitingAttachedRunsc {
|
|||
"attached runsc stage was already received",
|
||||
));
|
||||
}
|
||||
if self.helper_staged.is_some() && self.network_evidence.is_none() {
|
||||
if self.helper_staged.is_some() && self.network_attestation.is_none() {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"attached runsc cannot start before network attestation",
|
||||
));
|
||||
|
|
@ -469,6 +447,7 @@ impl AwaitingAttachedRunsc {
|
|||
lifetime: self.lifetime.take(),
|
||||
staged,
|
||||
network_evidence: self.network_evidence.take(),
|
||||
network_attestation: self.network_attestation.take(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -494,6 +473,7 @@ pub struct AwaitingSandboxReadiness {
|
|||
lifetime: Option<std::os::fd::OwnedFd>,
|
||||
staged: StagedProcessOwnership,
|
||||
network_evidence: Option<NetworkReadinessEvidence>,
|
||||
network_attestation: Option<NetworkReadinessAttestation>,
|
||||
}
|
||||
|
||||
impl AwaitingSandboxReadiness {
|
||||
|
|
@ -520,13 +500,35 @@ impl AwaitingSandboxReadiness {
|
|||
pub fn commit_networked(
|
||||
mut self,
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
network: &NetworkCoordinator<'_>,
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
) -> Result<(CommittedSandboxRuntime, NetworkReadinessEvidence), SandboxCoordinatorError> {
|
||||
let evidence =
|
||||
self.network_evidence
|
||||
let attestation =
|
||||
self.network_attestation
|
||||
.take()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"network readiness evidence is absent",
|
||||
"network attestation is absent",
|
||||
))?;
|
||||
let control = self
|
||||
.control
|
||||
.as_ref()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor control channel is absent",
|
||||
))?;
|
||||
let canary = match control.receive()? {
|
||||
SupervisorEvent::NetworkCanary(canary) => canary,
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
_ => {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not return owned-canary evidence",
|
||||
));
|
||||
}
|
||||
};
|
||||
let evidence = network.commit_readiness(policy, attestation, canary)?;
|
||||
control.acknowledge_network_readiness()?;
|
||||
self.network_evidence = Some(evidence.clone());
|
||||
let runtime = self.commit_after_probe(coordinator, Some(evidence.clone()))?;
|
||||
Ok((runtime, evidence))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
|
@ -11,21 +10,17 @@ use lazaret_core::identity::{
|
|||
ConfigurationFingerprint, ContainerId, CoordinatorId, DomainId, GenerationId, NetworkSidecarId,
|
||||
PrepareOperationId, SandboxId,
|
||||
};
|
||||
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
|
||||
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact, VolumeKind};
|
||||
use lazaret_linux::filesystem::namespace_identity;
|
||||
use lazaret_core::records::{DurableDecision, Generation, PreparedArtifact, VolumeKind};
|
||||
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_prepared_lower::{
|
||||
OciRegistryTransport, Platform, PreparedDestination, PreparedDestinationKind,
|
||||
PreparedLowerError, PreparedLowerStore, RegistryCredentials,
|
||||
};
|
||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
CREDENTIALS_DESTINATION, DEV_DESTINATION, ESCALATION_CLIENT_DESTINATION,
|
||||
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH,
|
||||
PROC_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION, SYS_DESTINATION,
|
||||
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
|
||||
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, PROC_DESTINATION, RootProcess,
|
||||
SANDBOX_INIT_DESTINATION, SYS_DESTINATION, TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
|
||||
};
|
||||
use lazaret_sandbox_contract::RUNTIME_FILE_DESTINATIONS;
|
||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||
|
|
@ -85,9 +80,6 @@ pub struct RuntimeSelection {
|
|||
artifact_authority: File,
|
||||
artifact_label: PathBuf,
|
||||
generation: Generation,
|
||||
resolver_addresses: Vec<IpAddress>,
|
||||
network_probe: NetworkProbeRequest,
|
||||
reachability: ReachabilityExpectation,
|
||||
container_id: ContainerId,
|
||||
escalation_client: File,
|
||||
sandbox_init: File,
|
||||
|
|
@ -121,9 +113,6 @@ impl RuntimeSelection {
|
|||
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
|
||||
)
|
||||
.map_err(failure)?,
|
||||
resolver_addresses: self.resolver_addresses.clone(),
|
||||
network_probe: self.network_probe.clone(),
|
||||
reachability: self.reachability.clone(),
|
||||
container_id: self.container_id,
|
||||
invoking_uid: rustix::process::getuid().as_raw(),
|
||||
escalation_client: &self.escalation_client,
|
||||
|
|
@ -222,9 +211,6 @@ async fn select_runtime_for_platform(
|
|||
sandbox_id,
|
||||
durable_decision: DurableDecision::None,
|
||||
};
|
||||
let namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)).map_err(failure)?;
|
||||
let (resolver_addresses, network_probe, reachability) =
|
||||
select_network_readiness(configuration, &generation, namespace);
|
||||
Ok(RuntimeSelection {
|
||||
store,
|
||||
domain_id: configuration.domain_id(),
|
||||
|
|
@ -234,9 +220,6 @@ async fn select_runtime_for_platform(
|
|||
artifact_authority,
|
||||
artifact_label,
|
||||
generation,
|
||||
resolver_addresses,
|
||||
network_probe,
|
||||
reachability,
|
||||
container_id: ContainerId::new(random_id()?),
|
||||
escalation_client,
|
||||
sandbox_init,
|
||||
|
|
@ -313,67 +296,6 @@ fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, RuntimeSel
|
|||
.ok_or_else(|| failure("generation identity exhausted"))
|
||||
}
|
||||
|
||||
fn select_network_readiness(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
generation: &Generation,
|
||||
enforcement_namespace: lazaret_core::identity::NamespaceIdentity,
|
||||
) -> (Vec<IpAddress>, NetworkProbeRequest, ReachabilityExpectation) {
|
||||
let readiness = configuration.network_readiness();
|
||||
let denied = readiness
|
||||
.denied
|
||||
.iter()
|
||||
.map(|target| TcpProbeTarget {
|
||||
address: IpAddress::V4(target.address),
|
||||
port: target.port,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let permitted = readiness
|
||||
.permitted
|
||||
.iter()
|
||||
.map(|target| TcpProbeTarget {
|
||||
address: IpAddress::V4(target.address),
|
||||
port: target.port,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let dns = readiness
|
||||
.dns
|
||||
.iter()
|
||||
.map(|target| DnsProbeTarget {
|
||||
resolver: IpAddress::V4(target.resolver),
|
||||
query_name: target.query.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let reachability = ReachabilityExpectation {
|
||||
denied_destinations: denied.iter().map(|target| target.address).collect(),
|
||||
permitted_destinations: permitted.iter().map(|target| target.address).collect(),
|
||||
dns_queries: dns
|
||||
.iter()
|
||||
.map(|target| DnsQueryExpectation {
|
||||
resolver: target.resolver,
|
||||
query_name: target.query_name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let resolvers = dns
|
||||
.iter()
|
||||
.map(|target| target.resolver)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
(
|
||||
resolvers,
|
||||
NetworkProbeRequest {
|
||||
domain_id: generation.domain_id,
|
||||
generation_id: generation.generation_id,
|
||||
enforcement_namespace,
|
||||
denied,
|
||||
permitted,
|
||||
dns,
|
||||
},
|
||||
reachability,
|
||||
)
|
||||
}
|
||||
|
||||
fn open_installed_executable(path: &Path) -> Result<File, RuntimeSelectionError> {
|
||||
let descriptor = openat2(
|
||||
rustix::fs::CWD,
|
||||
|
|
@ -420,6 +342,7 @@ fn random_id() -> Result<[u8; 32], RuntimeSelectionError> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::future::Future;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
|
@ -427,7 +350,6 @@ mod tests {
|
|||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
|
||||
use lazaret_core::identity::{DeviceId, InodeId, NamespaceIdentity};
|
||||
use lazaret_core::records::Domain;
|
||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
|
||||
|
|
@ -437,35 +359,9 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolved_readiness_maps_exactly_and_destinations_match_builder_policy() {
|
||||
fn destinations_match_builder_policy() {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let configuration = configuration(tree.path());
|
||||
let generation = Generation {
|
||||
domain_id: configuration.domain_id(),
|
||||
generation_id: GenerationId::new(9),
|
||||
configuration_fingerprint: configuration.configuration_fingerprint(),
|
||||
coordinator_id: CoordinatorId::new([1; 32]),
|
||||
network_sidecar_id: NetworkSidecarId::new([2; 32]),
|
||||
sandbox_id: SandboxId::new([3; 32]),
|
||||
durable_decision: DurableDecision::None,
|
||||
};
|
||||
let namespace = NamespaceIdentity {
|
||||
device: DeviceId::new(4),
|
||||
inode: InodeId::new(5),
|
||||
};
|
||||
let (resolvers, request, expectation) =
|
||||
select_network_readiness(&configuration, &generation, namespace);
|
||||
assert_eq!(resolvers, [IpAddress::V4([198, 51, 100, 53])]);
|
||||
assert_eq!(request.denied[0].port, 8443);
|
||||
assert_eq!(request.permitted[0].port, 443);
|
||||
assert_eq!(request.dns[0].query_name, "example.invalid");
|
||||
assert_eq!(expectation.denied_destinations, [request.denied[0].address]);
|
||||
assert_eq!(
|
||||
expectation.permitted_destinations,
|
||||
[request.permitted[0].address]
|
||||
);
|
||||
assert_eq!(expectation.dns_queries[0].resolver, request.dns[0].resolver);
|
||||
|
||||
let destinations = prepared_destinations(&configuration);
|
||||
assert!(destinations.iter().any(|destination| {
|
||||
destination.path == Path::new(WORKSPACE_DESTINATION)
|
||||
|
|
@ -620,7 +516,8 @@ mod tests {
|
|||
fs::write(
|
||||
&configuration_path,
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"Example.Invalid.\" }}]\n",
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"
|
||||
",
|
||||
root.join("data").display()
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,216 +1,7 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::io;
|
||||
use std::os::unix::ffi::OsStrExt as _;
|
||||
pub use lazaret_sandbox_init::{
|
||||
PRIVATE_SANDBOX_INIT_ROLE, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
|
||||
dispatch_private_sandbox_init, is_private_sandbox_init_role,
|
||||
};
|
||||
|
||||
use lazaret_linux::process::{TargetCommand, exec_target};
|
||||
|
||||
pub const PRIVATE_SANDBOX_INIT_ROLE: &str = "--lazaret-private-sandbox-init-v1";
|
||||
pub const PRIVATE_SESSION_EXEC_ROLE: &str = "--lazaret-private-session-exec-v1";
|
||||
pub const PRIVATE_SESSION_IDENTITY_ROLE: &str = "--lazaret-private-session-identity-v1";
|
||||
|
||||
#[must_use]
|
||||
pub fn is_private_sandbox_init_role(arguments: &[OsString]) -> bool {
|
||||
arguments.len() == 1 && arguments[0] == OsStr::new(PRIVATE_SANDBOX_INIT_ROLE)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dispatch_private_sandbox_init(arguments: &[OsString]) -> Option<io::Result<()>> {
|
||||
if is_private_sandbox_init_role(arguments) {
|
||||
loop {
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
if arguments
|
||||
.first()
|
||||
.is_some_and(|argument| argument == PRIVATE_SESSION_EXEC_ROLE)
|
||||
{
|
||||
return Some(run_session_exec(&arguments[1..]));
|
||||
}
|
||||
if arguments
|
||||
.first()
|
||||
.is_some_and(|argument| argument == PRIVATE_SESSION_IDENTITY_ROLE)
|
||||
{
|
||||
return Some(report_session_identity(&arguments[1..]));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn run_session_exec(arguments: &[OsString]) -> io::Result<()> {
|
||||
let (separator, argv) = arguments
|
||||
.split_first()
|
||||
.filter(|(separator, argv)| {
|
||||
separator == &OsStr::new("--")
|
||||
&& !argv.is_empty()
|
||||
&& !argv[0].is_empty()
|
||||
&& argv
|
||||
.iter()
|
||||
.all(|argument| !argument.as_bytes().contains(&0))
|
||||
})
|
||||
.ok_or_else(|| io::Error::other("session exec requires -- and a nonempty argv"))?;
|
||||
let _ = separator;
|
||||
establish_process_group_leader()?;
|
||||
let target = argv[1..]
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(TargetCommand::new(argv[0].clone()), TargetCommand::argument);
|
||||
Err(exec_target(target))
|
||||
}
|
||||
|
||||
fn establish_process_group_leader() -> io::Result<()> {
|
||||
let pid = rustix::process::getpid();
|
||||
if rustix::process::getpgrp() != pid {
|
||||
rustix::process::setsid()?;
|
||||
}
|
||||
require_process_group_leader(pid, rustix::process::getpgrp())
|
||||
}
|
||||
|
||||
fn require_process_group_leader(
|
||||
pid: rustix::process::Pid,
|
||||
process_group: rustix::process::Pid,
|
||||
) -> io::Result<()> {
|
||||
if pid != process_group {
|
||||
return Err(io::Error::other(
|
||||
"session exec is not its process-group leader",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_session_identity(arguments: &[OsString]) -> io::Result<()> {
|
||||
match arguments {
|
||||
[pid] => report_process_identity(parse_positive_pid(pid)?),
|
||||
[mode, group] if mode == "--group" => report_group_presence(parse_positive_pid(group)?),
|
||||
_ => Err(io::Error::other("session identity query is invalid")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_positive_pid(value: &OsStr) -> io::Result<u32> {
|
||||
value
|
||||
.to_str()
|
||||
.and_then(|value| value.parse::<u32>().ok())
|
||||
.filter(|value| *value > 0)
|
||||
.ok_or_else(|| io::Error::other("session identity number is invalid"))
|
||||
}
|
||||
|
||||
fn report_process_identity(pid: u32) -> io::Result<()> {
|
||||
let Some((observed_pid, start_ticks, group)) = read_process_identity(pid)? else {
|
||||
println!("absent");
|
||||
return Ok(());
|
||||
};
|
||||
println!("{observed_pid} {start_ticks} {group}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_group_presence(group: u32) -> io::Result<()> {
|
||||
println!(
|
||||
"{}",
|
||||
if group_is_present_except(group, std::process::id())? {
|
||||
"present"
|
||||
} else {
|
||||
"absent"
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn group_is_present_except(group: u32, observer: u32) -> io::Result<bool> {
|
||||
for entry in std::fs::read_dir("/proc")? {
|
||||
let entry = entry?;
|
||||
let Some(pid) = entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.and_then(|value| value.parse::<u32>().ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if pid == observer {
|
||||
continue;
|
||||
}
|
||||
if read_process_identity(pid)?.is_some_and(|(_, _, observed_group)| observed_group == group)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_process_identity(pid: u32) -> io::Result<Option<(u32, u64, u32)>> {
|
||||
let stat = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
|
||||
Ok(stat) => stat,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let close = stat
|
||||
.rfind(')')
|
||||
.ok_or_else(|| io::Error::other("guest process stat is malformed"))?;
|
||||
let observed_pid = stat[..close]
|
||||
.split_once(' ')
|
||||
.and_then(|(pid, _)| pid.parse::<u32>().ok())
|
||||
.ok_or_else(|| io::Error::other("guest process stat PID is malformed"))?;
|
||||
let fields = stat[close + 1..].split_whitespace().collect::<Vec<_>>();
|
||||
let group = fields
|
||||
.get(2)
|
||||
.and_then(|value| value.parse::<u32>().ok())
|
||||
.ok_or_else(|| io::Error::other("guest process stat group is malformed"))?;
|
||||
let start_ticks = fields
|
||||
.get(19)
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.ok_or_else(|| io::Error::other("guest process stat start is malformed"))?;
|
||||
Ok(Some((observed_pid, start_ticks, group)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn private_role_dispatch_is_exact_and_closed() {
|
||||
assert!(is_private_sandbox_init_role(&[OsString::from(
|
||||
PRIVATE_SANDBOX_INIT_ROLE
|
||||
)]));
|
||||
assert!(!is_private_sandbox_init_role(&[]));
|
||||
assert!(!is_private_sandbox_init_role(&[
|
||||
OsString::from(PRIVATE_SANDBOX_INIT_ROLE),
|
||||
OsString::from("extra"),
|
||||
]));
|
||||
assert!(!is_private_sandbox_init_role(&[OsString::from(
|
||||
"--private-sandbox-init"
|
||||
)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_session_roles_are_shape_separated() {
|
||||
assert!(dispatch_private_sandbox_init(&[]).is_none());
|
||||
assert!(
|
||||
dispatch_private_sandbox_init(&[
|
||||
OsString::from(PRIVATE_SESSION_EXEC_ROLE),
|
||||
OsString::from("missing-separator"),
|
||||
])
|
||||
.unwrap()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
dispatch_private_sandbox_init(&[
|
||||
OsString::from(PRIVATE_SESSION_IDENTITY_ROLE),
|
||||
OsString::from("not-a-pid"),
|
||||
])
|
||||
.unwrap()
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_group_leader_validation_is_fail_closed() {
|
||||
let leader = rustix::process::Pid::from_raw(41).unwrap();
|
||||
let other_group = rustix::process::Pid::from_raw(42).unwrap();
|
||||
require_process_group_leader(leader, leader).unwrap();
|
||||
assert!(require_process_group_leader(leader, other_group).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_presence_is_independent_of_a_designated_leader() {
|
||||
let (_, _, group) = read_process_identity(std::process::id()).unwrap().unwrap();
|
||||
assert!(group_is_present_except(group, u32::MAX).unwrap());
|
||||
assert!(!group_is_present_except(u32::MAX, u32::MAX).unwrap());
|
||||
}
|
||||
}
|
||||
pub(crate) const DEFAULT_AGENT_PATH: &str =
|
||||
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||
|
|
|
|||
|
|
@ -699,7 +699,8 @@ fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomain
|
|||
fs::write(
|
||||
&configuration_path,
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]
|
||||
",
|
||||
tree.path.join("data").display()
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::{HashMap, VecDeque};
|
||||
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
|
|
@ -6,6 +6,7 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazaret_core::configuration::EnvironmentValue;
|
||||
use lazaret_core::identity::{
|
||||
ContainerId, CoordinatorId, DeviceId, DomainId, FileDescriptor, GenerationId,
|
||||
GuestProcessGroupId, GuestProcessId, GuestProcessStartTimeTicks, InodeId,
|
||||
|
|
@ -467,6 +468,7 @@ pub struct TerminalAcquisition {
|
|||
pub host_coordinator: PidfsProcessIdentityV1,
|
||||
pub runsc_program: OsString,
|
||||
pub runsc_global_arguments: Vec<OsString>,
|
||||
pub environment: BTreeMap<String, EnvironmentValue>,
|
||||
pub internal_pid_file: PathBuf,
|
||||
pub argv: Vec<Vec<u8>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use super::{
|
||||
Arc, ContainerId, ControlError, CoordinatorId, DomainId, GenerationId, HashMap,
|
||||
HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder, InteractiveControlSession,
|
||||
InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionExecutor,
|
||||
InteractiveSessionMessage, InteractiveSessionRequest, Mutex, OsString, PathBuf,
|
||||
PidfsProcessIdentityV1, PromptRouteCancellation, ProtocolVersion, RunscSessionTerminalFactory,
|
||||
SessionId, SessionPromptTransport, SessionTerminal, SessionTerminalEvent,
|
||||
SessionTerminalFactory, SessionTerminalMessage, SessionTerminalProcess, TerminalAcquisition,
|
||||
TerminalError, TerminalLoss, TerminalRoute, VecDeque,
|
||||
Arc, BTreeMap, ContainerId, ControlError, CoordinatorId, DomainId, EnvironmentValue,
|
||||
GenerationId, HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder,
|
||||
InteractiveControlSession, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionExecutor, InteractiveSessionMessage, InteractiveSessionRequest, Mutex,
|
||||
OsString, PathBuf, PidfsProcessIdentityV1, PromptRouteCancellation, ProtocolVersion,
|
||||
RunscSessionTerminalFactory, SessionId, SessionPromptTransport, SessionTerminal,
|
||||
SessionTerminalEvent, SessionTerminalFactory, SessionTerminalMessage, SessionTerminalProcess,
|
||||
TerminalAcquisition, TerminalError, TerminalLoss, TerminalRoute, VecDeque,
|
||||
};
|
||||
|
||||
pub(super) struct ActiveTerminal {
|
||||
|
|
@ -36,6 +36,7 @@ pub struct RunscInteractiveSessionExecutor {
|
|||
host_coordinator: PidfsProcessIdentityV1,
|
||||
runsc_program: OsString,
|
||||
runsc_global_arguments: Vec<OsString>,
|
||||
environment: BTreeMap<String, EnvironmentValue>,
|
||||
runtime_root: PathBuf,
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ impl RunscInteractiveSessionExecutor {
|
|||
container_id: ContainerId,
|
||||
host_coordinator: PidfsProcessIdentityV1,
|
||||
command: &lazaret_runtime_supervisor::sandbox::RunscCommand,
|
||||
environment: BTreeMap<String, EnvironmentValue>,
|
||||
runtime_root: PathBuf,
|
||||
) -> Result<Self, TerminalError> {
|
||||
let run = command
|
||||
|
|
@ -75,6 +77,7 @@ impl RunscInteractiveSessionExecutor {
|
|||
host_coordinator,
|
||||
runsc_program: command.program().to_owned(),
|
||||
runsc_global_arguments: command.arguments()[..run].to_vec(),
|
||||
environment,
|
||||
runtime_root,
|
||||
})
|
||||
}
|
||||
|
|
@ -104,6 +107,7 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
|||
host_coordinator: self.host_coordinator,
|
||||
runsc_program: self.runsc_program.clone(),
|
||||
runsc_global_arguments: self.runsc_global_arguments.clone(),
|
||||
environment: self.environment.clone(),
|
||||
internal_pid_file: self.runtime_root.join(format!(
|
||||
"session-{}.pid",
|
||||
hex_session(request.session_id.as_bytes())
|
||||
|
|
|
|||
|
|
@ -45,8 +45,19 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
|
|||
for argument in &request.runsc_global_arguments {
|
||||
target = target.argument(argument.clone());
|
||||
}
|
||||
target = target.argument("exec");
|
||||
if !request.environment.contains_key("PATH") {
|
||||
target = target
|
||||
.argument("-env")
|
||||
.argument(format!("PATH={}", crate::sandbox_init::DEFAULT_AGENT_PATH));
|
||||
}
|
||||
for (name, value) in &request.environment {
|
||||
let mut assignment = OsString::from(name);
|
||||
assignment.push("=");
|
||||
assignment.push(value.expose());
|
||||
target = target.argument("-env").argument(assignment);
|
||||
}
|
||||
target = target
|
||||
.argument("exec")
|
||||
.argument(format!(
|
||||
"-internal-pid-file={}",
|
||||
request.internal_pid_file.display()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue