lazaret/crates/lazaret-core/tests/network.rs

475 lines
17 KiB
Rust
Raw Normal View History

use std::fmt::Write;
use lazaret_core::identity::{
BootId, ConfigurationFingerprint, DerivedPolicyDigest, DeviceId, DomainId, GenerationId,
HostPidLocator, InodeId, NamespaceIdentity, NetworkSidecarId, PidfsDeviceId, PidfsInodeId,
PidfsProcessAuthorityV1, PidfsProcessIdentityV1, ProcessStartTimeTicks, ReadinessFingerprint,
};
use lazaret_core::network::{
NetworkValidationErrorKind, ReadinessExpectation, accept_network_readiness,
canonical_readiness_identity_bytes, derive_policy, readiness_fingerprint, readiness_identity,
validate_network_readiness_evidence,
};
use lazaret_core::records::{
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction,
DerivedNetworkPolicyFields, IpAddress, IpNetwork, NetworkCanaryObservation, NetworkObservation,
NetworkReadinessAttestation, NetworkReadinessAttestationFields, PolicyAction, PolicyDelivery,
PrefixRule, Resolver,
};
type AttestationMutation = fn(&mut NetworkReadinessAttestation);
type EvidenceMutation = fn(&mut lazaret_core::records::NetworkReadinessEvidence);
type CanaryMutation = fn(&mut NetworkCanaryObservation);
fn repeated(value: u8) -> [u8; 32] {
[value; 32]
}
fn namespace() -> NamespaceIdentity {
NamespaceIdentity {
device: DeviceId::new(41),
inode: InodeId::new(42),
}
}
fn process(value: u8) -> PidfsProcessIdentityV1 {
PidfsProcessIdentityV1::new(
PidfsProcessAuthorityV1 {
boot_id: BootId::new([value; 16]),
pidfs_st_dev: PidfsDeviceId::new(u64::from(value)),
pidfs_st_ino: PidfsInodeId::new(u64::from(value) + 1),
},
HostPidLocator::new(u32::from(value)),
ProcessStartTimeTicks::new(u64::from(value) + 2),
)
}
fn policy_fields() -> DerivedNetworkPolicyFields {
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: vec![
PrefixRule {
family: AddressFamily::Ipv4,
network: IpNetwork {
address: IpAddress::V4([10, 0, 0, 0]),
prefix_length: 8,
},
action: PolicyAction::Deny,
},
PrefixRule {
family: AddressFamily::Ipv4,
network: IpNetwork {
address: IpAddress::V4([10, 20, 0, 0]),
prefix_length: 16,
},
action: PolicyAction::Allow,
},
PrefixRule {
family: AddressFamily::Ipv4,
network: IpNetwork {
address: IpAddress::V4([172, 16, 0, 0]),
prefix_length: 12,
},
action: PolicyAction::Deny,
},
PrefixRule {
family: AddressFamily::Ipv4,
network: IpNetwork {
address: IpAddress::V4([192, 168, 0, 0]),
prefix_length: 16,
},
action: PolicyAction::Deny,
},
],
resolvers: vec![Resolver {
family: AddressFamily::Ipv4,
address: IpAddress::V4([192, 0, 2, 53]),
}],
}
}
struct Fixture {
policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
runtime_supervisor: PidfsProcessIdentityV1,
connectivity_helper: PidfsProcessIdentityV1,
attestation: NetworkReadinessAttestation,
canary: NetworkCanaryObservation,
}
impl Fixture {
fn new() -> Self {
let policy = derive_policy(policy_fields()).unwrap();
let runtime_supervisor = process(51);
let connectivity_helper = process(61);
let attestation = NetworkReadinessAttestation::v1(NetworkReadinessAttestationFields {
domain_id: DomainId::new(repeated(1)),
generation_id: GenerationId::new(2),
configuration_fingerprint: ConfigurationFingerprint::new(repeated(3)),
network_sidecar_id: NetworkSidecarId::new(repeated(4)),
derived_policy_digest: policy.digest(),
enforcement_namespace: namespace(),
runtime_supervisor,
connectivity_helper,
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,
},
});
let canary = NetworkCanaryObservation {
domain_id: DomainId::new(repeated(1)),
generation_id: GenerationId::new(2),
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,
};
Self {
policy,
runtime_supervisor,
connectivity_helper,
attestation,
canary,
}
}
fn expectation(&self) -> ReadinessExpectation<'_> {
ReadinessExpectation {
domain_id: DomainId::new(repeated(1)),
generation_id: GenerationId::new(2),
configuration_fingerprint: ConfigurationFingerprint::new(repeated(3)),
network_sidecar_id: NetworkSidecarId::new(repeated(4)),
enforcement_namespace: namespace(),
runtime_supervisor: &self.runtime_supervisor,
connectivity_helper: &self.connectivity_helper,
}
}
fn evidence(&self) -> lazaret_core::records::NetworkReadinessEvidence {
accept_network_readiness(
&self.policy,
&self.expectation(),
self.attestation.clone(),
self.canary.clone(),
)
.unwrap()
}
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().fold(String::new(), |mut encoded, byte| {
write!(encoded, "{byte:02x}").unwrap();
encoded
})
}
#[test]
fn canonical_policy_and_readiness_vectors_are_stable() {
let fixture = Fixture::new();
let identity = readiness_identity(&fixture.attestation);
assert_eq!(
hex(fixture.policy.canonical_bytes()),
"216c617a617265742d646572697665642d6e6574776f726b2d706f6c6963792d763102000001010100000400000a000000080100000a14000010000000ac1000000c010000c0a800001001010000c0000235"
);
assert_eq!(
hex(fixture.policy.digest().as_bytes()),
"01b72b130912b96c591271308b6b54f99431b90b0f6eee252eb4ada6aa0a42e3"
);
assert_eq!(
hex(&canonical_readiness_identity_bytes(&identity).unwrap()),
"256c617a617265742d6e6574776f726b2d72656164696e6573732d6964656e746974792d76310101010101010101010101010101010101010101010101010101010101010101020303030303030303030303030303030303030303030303030303030303030303040404040404040404040404040404040404040404040404040404040404040401b72b130912b96c591271308b6b54f99431b90b0f6eee252eb4ada6aa0a42e3292a"
);
assert_eq!(
hex(readiness_fingerprint(&identity).unwrap().as_bytes()),
"1faf0b0bb7807002e223a113d3da93520bfe91b350380d5d890e20caed3d95af"
);
}
#[test]
fn semantically_reordered_policy_has_one_identity() {
let expected = derive_policy(policy_fields()).unwrap();
let mut reordered = policy_fields();
reordered.address_families.reverse();
reordered.prefix_rules.reverse();
let actual = derive_policy(reordered).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn malformed_policy_inputs_are_refused() {
let cases = [
("missing family", {
let mut fields = policy_fields();
fields.address_families.pop();
fields
}),
("enabled IPv6", {
let mut fields = policy_fields();
fields.address_families[1].disposition = AddressFamilyDispositionValue::Enabled;
fields
}),
("wrong default", {
let mut fields = policy_fields();
fields.default_actions[0].action = PolicyAction::Deny;
fields
}),
("missing default private deny", {
let mut fields = policy_fields();
fields.prefix_rules.pop();
fields
}),
("noncanonical CIDR", {
let mut fields = policy_fields();
fields.prefix_rules[0].network.address = IpAddress::V4([10, 0, 0, 1]);
fields
}),
("oversized prefix", {
let mut fields = policy_fields();
fields.prefix_rules[0].network.prefix_length = 33;
fields
}),
("prefix family mismatch", {
let mut fields = policy_fields();
fields.prefix_rules[0].network.address = IpAddress::V6([0; 16]);
fields
}),
("duplicate prefix", {
let mut fields = policy_fields();
fields.prefix_rules.push(fields.prefix_rules[0]);
fields
}),
("conflicting prefix", {
let mut fields = policy_fields();
let mut conflicting = fields.prefix_rules[0];
conflicting.action = PolicyAction::Allow;
fields.prefix_rules.push(conflicting);
fields
}),
("no resolver", {
let mut fields = policy_fields();
fields.resolvers.clear();
fields
}),
("resolver family mismatch", {
let mut fields = policy_fields();
fields.resolvers[0].address = IpAddress::V6([0; 16]);
fields
}),
("duplicate resolver", {
let mut fields = policy_fields();
fields.resolvers.push(fields.resolvers[0]);
fields
}),
];
for (name, fields) in cases {
assert!(derive_policy(fields).is_err(), "accepted {name}");
}
}
#[test]
fn every_attestation_identity_mismatch_is_refused() {
let fixture = Fixture::new();
let mutations: [(&str, AttestationMutation); 9] = [
("domain", |value| {
value.domain_id = DomainId::new(repeated(71));
}),
("generation", |value| {
value.generation_id = GenerationId::new(72);
}),
("configuration", |value| {
value.configuration_fingerprint = ConfigurationFingerprint::new(repeated(73));
}),
("sidecar", |value| {
value.network_sidecar_id = NetworkSidecarId::new(repeated(74));
}),
("policy", |value| {
value.derived_policy_digest = DerivedPolicyDigest::new(repeated(75));
}),
("namespace", |value| {
value.enforcement_namespace.inode = InodeId::new(76);
}),
("namespace device", |value| {
value.enforcement_namespace.device = DeviceId::new(77);
}),
("runtime supervisor", |value| {
value.runtime_supervisor = process(78);
}),
("connectivity helper", |value| {
value.connectivity_helper = process(79);
}),
];
for (name, mutate) in mutations {
let mut attestation = fixture.attestation.clone();
mutate(&mut attestation);
let error = accept_network_readiness(
&fixture.policy,
&fixture.expectation(),
attestation,
fixture.canary.clone(),
)
.unwrap_err();
assert_eq!(
error.kind(),
NetworkValidationErrorKind::Attestation,
"{name}"
);
}
}
#[test]
fn every_evidence_identity_mismatch_is_refused() {
let fixture = Fixture::new();
let mutations: [(&str, EvidenceMutation); 13] = [
("outer domain", |value| {
value.domain_id = DomainId::new(repeated(81));
}),
("outer generation", |value| {
value.generation_id = GenerationId::new(82);
}),
("outer sidecar", |value| {
value.network_sidecar_id = NetworkSidecarId::new(repeated(83));
}),
("identity domain", |value| {
value.readiness_identity.domain_id = DomainId::new(repeated(84));
}),
("identity generation", |value| {
value.readiness_identity.generation_id = GenerationId::new(85);
}),
("identity configuration", |value| {
value.readiness_identity.configuration_fingerprint =
ConfigurationFingerprint::new(repeated(86));
}),
("identity sidecar", |value| {
value.readiness_identity.network_sidecar_id = NetworkSidecarId::new(repeated(87));
}),
("identity policy", |value| {
value.readiness_identity.derived_policy_digest = DerivedPolicyDigest::new(repeated(88));
}),
("identity namespace", |value| {
value.readiness_identity.enforcement_namespace.inode = InodeId::new(89);
}),
("fingerprint", |value| {
value.readiness_fingerprint = ReadinessFingerprint::new(repeated(90));
}),
("probe domain", |value| {
value.canary_observation.domain_id = DomainId::new(repeated(91));
}),
("probe generation", |value| {
value.canary_observation.generation_id = GenerationId::new(92);
}),
("probe namespace", |value| {
value.canary_observation.enforcement_namespace.inode = InodeId::new(93);
}),
];
for (name, mutate) in mutations {
let mut evidence = fixture.evidence();
mutate(&mut evidence);
assert!(
validate_network_readiness_evidence(
&fixture.policy,
&fixture.expectation(),
&evidence,
)
.is_err(),
"accepted {name}"
);
}
}
#[test]
fn incomplete_or_failed_canary_is_refused() {
let fixture = Fixture::new();
let mutations: [(&str, CanaryMutation); 6] = [
("deny reached", |value| {
value.denied_endpoint_unreachable = false;
}),
("permit unreachable", |value| {
value.allowed_endpoint_authenticated = false;
}),
("policy mismatch", |value| {
value.linux_policy_matches = false;
}),
("cleanup mismatch", |value| {
value.cleanup_shape_matched = false;
}),
("capability retained", |value| {
value.network_admin_dropped = false;
}),
("IPv6 present", |value| value.ipv6_absent = false),
];
for (name, mutate) in mutations {
let mut canary = fixture.canary.clone();
mutate(&mut canary);
assert!(
accept_network_readiness(
&fixture.policy,
&fixture.expectation(),
fixture.attestation.clone(),
canary,
)
.is_err(),
"accepted {name}"
);
}
}
#[test]
fn incomplete_or_malformed_attestation_is_refused() {
let fixture = Fixture::new();
let mutations: [(&str, AttestationMutation); 3] = [
("helper not ready", |value| {
value.observation.helper_ready = false;
}),
("routes not attested", |value| {
value.observation.routes_attested = false;
}),
("noncanonical route", |value| {
value.observation.observed_routes[0].address = IpAddress::V4([10, 0, 0, 1]);
}),
];
for (name, mutate) in mutations {
let mut attestation = fixture.attestation.clone();
mutate(&mut attestation);
assert!(
accept_network_readiness(
&fixture.policy,
&fixture.expectation(),
attestation,
fixture.canary.clone(),
)
.is_err(),
"accepted {name}"
);
}
}