Implement supervised rootless network readiness
This commit is contained in:
parent
407b75b1ff
commit
ba6a075839
1 changed files with 2917 additions and 54 deletions
244
crates/lazaret-host-coordinator/src/network/tests.rs
Normal file
244
crates/lazaret-host-coordinator/src/network/tests.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
use std::fs::{File, OpenOptions};
|
||||
|
||||
use lazaret_core::identity::{
|
||||
CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId, HostPath, NamespaceIdentity,
|
||||
NetworkSidecarId, PidfsProcessIdentityV1, SandboxId,
|
||||
};
|
||||
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation, derive_policy};
|
||||
use lazaret_core::records::{
|
||||
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction,
|
||||
DeniedDestinationObservation, DerivedNetworkPolicyFields, DnsObservation, Domain,
|
||||
DurableDecision, Generation, HostProcessOwnershipSlot, IpAddress, IpNetwork,
|
||||
NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation,
|
||||
NetworkReadinessAttestationFields, ParticipantOwnership, ParticipantRole, ParticipantState,
|
||||
PermittedDestinationObservation, PolicyAction, PolicyDelivery, PrefixRule, Resolver,
|
||||
StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
|
||||
use lazaret_linux::process::current_process_identity;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Fixture {
|
||||
_tree: TempDir,
|
||||
store: HostCoordinatorDomainStore,
|
||||
generation: Generation,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let domain = Domain {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
profile_name: "network-test".into(),
|
||||
canonical_workspace: HostPath::new(b"/workspace".to_vec()),
|
||||
};
|
||||
let generation = Generation {
|
||||
domain_id: domain.domain_id,
|
||||
generation_id: GenerationId::new(7),
|
||||
configuration_fingerprint: lazaret_core::identity::ConfigurationFingerprint::new(
|
||||
[2; 32],
|
||||
),
|
||||
coordinator_id: CoordinatorId::new([3; 32]),
|
||||
network_sidecar_id: NetworkSidecarId::new([4; 32]),
|
||||
sandbox_id: SandboxId::new([5; 32]),
|
||||
durable_decision: DurableDecision::None,
|
||||
};
|
||||
let lock = try_exclusive_flock(lock_file(&tree.path().join("domain.lock"))).unwrap();
|
||||
let store =
|
||||
HostCoordinatorDomainStore::open(lock, &tree.path().join("domain.redb"), &domain)
|
||||
.unwrap();
|
||||
store
|
||||
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
|
||||
.unwrap();
|
||||
Self {
|
||||
_tree: tree,
|
||||
store,
|
||||
generation,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_roles(&self) -> (CanonicalDerivedNetworkPolicy, PidfsProcessIdentityV1) {
|
||||
let policy = policy();
|
||||
let runtime = current_process_identity().unwrap();
|
||||
let coordinator = NetworkCoordinator::new(&self.store);
|
||||
coordinator
|
||||
.begin_helper_intent(
|
||||
self.generation.generation_id,
|
||||
self.generation.network_sidecar_id,
|
||||
runtime,
|
||||
)
|
||||
.unwrap();
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
self.generation.domain_id,
|
||||
self.generation.generation_id,
|
||||
self.generation.coordinator_id,
|
||||
HostProcessOwnershipSlot::HelperConnectivityHelper,
|
||||
runtime,
|
||||
);
|
||||
coordinator.stage_helper(staged, runtime).unwrap();
|
||||
assert!(
|
||||
coordinator
|
||||
.acknowledge_helper(staged)
|
||||
.unwrap()
|
||||
.acknowledges(&staged)
|
||||
);
|
||||
coordinator.commit_helper(staged).unwrap();
|
||||
coordinator
|
||||
.begin_network_intent(self.generation.generation_id, policy.digest(), runtime)
|
||||
.unwrap();
|
||||
(policy, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_probe_leaves_network_intent_uncommitted() {
|
||||
let fixture = Fixture::new();
|
||||
let (policy, runtime) = fixture.prepare_roles();
|
||||
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
|
||||
let attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
|
||||
let probe = NetworkReachabilityProbe {
|
||||
domain_id: fixture.generation.domain_id,
|
||||
generation_id: fixture.generation.generation_id,
|
||||
enforcement_namespace: namespace,
|
||||
denied_destinations: vec![DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 1, 2, 3]),
|
||||
unreachable: false,
|
||||
}],
|
||||
permitted_destinations: vec![PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([1, 1, 1, 1]),
|
||||
reachable: true,
|
||||
}],
|
||||
dns_observations: vec![DnsObservation {
|
||||
resolver: IpAddress::V4([8, 8, 8, 8]),
|
||||
query_name: "example.com".into(),
|
||||
resolved_addresses: vec![IpAddress::V4([93, 184, 216, 34])],
|
||||
}],
|
||||
};
|
||||
let result = NetworkCoordinator::new(&fixture.store).commit_readiness(
|
||||
&policy,
|
||||
attestation,
|
||||
probe,
|
||||
&reachability(),
|
||||
);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NetworkCoordinatorError::InvalidEvidence(_))
|
||||
));
|
||||
let participant = fixture
|
||||
.store
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.participants
|
||||
.into_iter()
|
||||
.find(|participant| participant.role() == ParticipantRole::Network)
|
||||
.unwrap();
|
||||
assert_eq!(participant.state(), ParticipantState::Intent);
|
||||
let Some(ParticipantOwnership::Network(ownership)) = participant.ownership() else {
|
||||
panic!("network ownership missing");
|
||||
};
|
||||
assert!(ownership.readiness_evidence.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independently_observed_namespace_refuses_attestation_mutation() {
|
||||
let fixture = Fixture::new();
|
||||
let (policy, runtime) = fixture.prepare_roles();
|
||||
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
|
||||
let mut attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
|
||||
attestation.enforcement_namespace.inode =
|
||||
lazaret_core::identity::InodeId::new(attestation.enforcement_namespace.inode.get() + 1);
|
||||
assert!(matches!(
|
||||
NetworkCoordinator::new(&fixture.store).validate_attestation(&policy, &attestation),
|
||||
Err(NetworkCoordinatorError::InvalidState(_))
|
||||
| Err(NetworkCoordinatorError::InvalidEvidence(_))
|
||||
));
|
||||
}
|
||||
|
||||
fn policy() -> CanonicalDerivedNetworkPolicy {
|
||||
derive_policy(DerivedNetworkPolicyFields {
|
||||
address_families: vec![
|
||||
AddressFamilyDisposition {
|
||||
family: AddressFamily::Ipv4,
|
||||
disposition: AddressFamilyDispositionValue::Enabled,
|
||||
},
|
||||
AddressFamilyDisposition {
|
||||
family: AddressFamily::Ipv6,
|
||||
disposition: AddressFamilyDispositionValue::Disabled,
|
||||
},
|
||||
],
|
||||
default_actions: vec![DefaultAction {
|
||||
family: AddressFamily::Ipv4,
|
||||
action: PolicyAction::Allow,
|
||||
}],
|
||||
prefix_rules: [
|
||||
([10, 0, 0, 0], 8),
|
||||
([172, 16, 0, 0], 12),
|
||||
([192, 168, 0, 0], 16),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(address, prefix_length)| PrefixRule {
|
||||
family: AddressFamily::Ipv4,
|
||||
network: IpNetwork {
|
||||
address: IpAddress::V4(address),
|
||||
prefix_length,
|
||||
},
|
||||
action: PolicyAction::Deny,
|
||||
})
|
||||
.collect(),
|
||||
resolvers: vec![Resolver {
|
||||
family: AddressFamily::Ipv4,
|
||||
address: IpAddress::V4([8, 8, 8, 8]),
|
||||
}],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn attestation(
|
||||
generation: &Generation,
|
||||
digest: DerivedPolicyDigest,
|
||||
namespace: NamespaceIdentity,
|
||||
runtime: PidfsProcessIdentityV1,
|
||||
) -> NetworkReadinessAttestation {
|
||||
NetworkReadinessAttestation::v1(NetworkReadinessAttestationFields {
|
||||
domain_id: generation.domain_id,
|
||||
generation_id: generation.generation_id,
|
||||
configuration_fingerprint: generation.configuration_fingerprint,
|
||||
network_sidecar_id: generation.network_sidecar_id,
|
||||
derived_policy_digest: digest,
|
||||
enforcement_namespace: namespace,
|
||||
runtime_supervisor: runtime,
|
||||
connectivity_helper: runtime,
|
||||
observation: NetworkObservation {
|
||||
policy_delivery: PolicyDelivery::SealedMemfd,
|
||||
helper_ready: true,
|
||||
observed_routes: vec![IpNetwork {
|
||||
address: IpAddress::V4([10, 0, 0, 0]),
|
||||
prefix_length: 8,
|
||||
}],
|
||||
routes_attested: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn reachability() -> ReachabilityExpectation {
|
||||
ReachabilityExpectation {
|
||||
denied_destinations: vec![IpAddress::V4([10, 1, 2, 3])],
|
||||
permitted_destinations: vec![IpAddress::V4([1, 1, 1, 1])],
|
||||
dns_queries: vec![DnsQueryExpectation {
|
||||
resolver: IpAddress::V4([8, 8, 8, 8]),
|
||||
query_name: "example.com".into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_file(path: &std::path::Path) -> File {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(path)
|
||||
.unwrap()
|
||||
}
|
||||
Loading…
Reference in a new issue