Replace external reachability with an owned canary
This commit is contained in:
parent
0f6825c3b9
commit
31e9ab0f2d
3 changed files with 1409 additions and 1972 deletions
|
|
@ -11,9 +11,7 @@ use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, Stat, openat2};
|
|||
|
||||
use crate::identity::HostPath;
|
||||
use crate::records::{
|
||||
EnvironmentReference, NetworkProbeTargetConfigurationV1, NetworkReadinessConfigurationV1,
|
||||
ProfileConfigurationV1, ResolvedAutoApprovalRule, ResolvedDnsProbeTargetV1,
|
||||
ResolvedNetworkProbeTargetV1, ResolvedNetworkReadinessV1, ResolvedVolume,
|
||||
EnvironmentReference, ProfileConfigurationV1, ResolvedAutoApprovalRule, ResolvedVolume,
|
||||
TrustedHostConfigurationV1, VolumeAccess, VolumeKind,
|
||||
};
|
||||
|
||||
|
|
@ -454,173 +452,6 @@ fn ipv4_prefixes_overlap(left: Ipv4Prefix, right: Ipv4Prefix) -> bool {
|
|||
u32::from_be_bytes(left.address()) & mask == u32::from_be_bytes(right.address()) & mask
|
||||
}
|
||||
|
||||
const MAX_READINESS_TARGETS: usize = 32;
|
||||
pub(super) fn resolve_network_readiness(
|
||||
readiness: &NetworkReadinessConfigurationV1,
|
||||
allow: &[Ipv4Prefix],
|
||||
deny: &[Ipv4Prefix],
|
||||
) -> Result<ResolvedNetworkReadinessV1, ConfigurationError> {
|
||||
if readiness.denied.is_empty() || readiness.permitted.is_empty() || readiness.dns.is_empty() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness requires denied, permitted, and DNS targets",
|
||||
));
|
||||
}
|
||||
if readiness.denied.len() > MAX_READINESS_TARGETS
|
||||
|| readiness.permitted.len() > MAX_READINESS_TARGETS
|
||||
|| readiness.dns.len() > MAX_READINESS_TARGETS
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness target count exceeds 32",
|
||||
));
|
||||
}
|
||||
|
||||
let denied = resolve_tcp_readiness_targets(&readiness.denied, allow, deny, false)?;
|
||||
let permitted = resolve_tcp_readiness_targets(&readiness.permitted, allow, deny, true)?;
|
||||
|
||||
let dns = readiness
|
||||
.dns
|
||||
.iter()
|
||||
.map(|target| {
|
||||
Ok(ResolvedDnsProbeTargetV1 {
|
||||
resolver: parse_probe_address(&target.resolver)?,
|
||||
query: normalize_dns_query(&target.query)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, ConfigurationError>>()?;
|
||||
if dns.len() != readiness.dns.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness contains duplicate DNS targets",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ResolvedNetworkReadinessV1 {
|
||||
denied: denied.into_iter().collect(),
|
||||
permitted: permitted.into_iter().collect(),
|
||||
dns: dns.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_tcp_readiness_targets(
|
||||
targets: &[NetworkProbeTargetConfigurationV1],
|
||||
allow: &[Ipv4Prefix],
|
||||
deny: &[Ipv4Prefix],
|
||||
expected_allowed: bool,
|
||||
) -> Result<BTreeSet<ResolvedNetworkProbeTargetV1>, ConfigurationError> {
|
||||
let resolved = targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
if target.port == 0 {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP target has port zero",
|
||||
));
|
||||
}
|
||||
let address = parse_probe_address(&target.address)?;
|
||||
if is_effectively_allowed(address, allow, deny) != expected_allowed {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP target disagrees with effective profile policy",
|
||||
));
|
||||
}
|
||||
Ok(ResolvedNetworkProbeTargetV1 {
|
||||
address,
|
||||
port: target.port,
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
if resolved.len() != targets.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness contains duplicate TCP targets",
|
||||
));
|
||||
}
|
||||
if resolved
|
||||
.iter()
|
||||
.map(|target| target.address)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len()
|
||||
!= resolved.len()
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP targets repeat an observation address",
|
||||
));
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn parse_probe_address(value: &str) -> Result<[u8; 4], ConfigurationError> {
|
||||
let address = value.parse::<std::net::Ipv4Addr>().map_err(|_| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness target address is not IPv4",
|
||||
)
|
||||
})?;
|
||||
if address.is_unspecified() || address.is_multicast() || address.is_broadcast() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness target address is not unicast",
|
||||
));
|
||||
}
|
||||
Ok(address.octets())
|
||||
}
|
||||
|
||||
fn normalize_dns_query(value: &str) -> Result<String, ConfigurationError> {
|
||||
let query = value.strip_suffix('.').unwrap_or(value);
|
||||
if query.is_empty() || query.len() > 253 || !query.is_ascii() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"DNS readiness query name is invalid",
|
||||
));
|
||||
}
|
||||
for label in query.split('.') {
|
||||
if label.is_empty()
|
||||
|| label.len() > 63
|
||||
|| label.starts_with('-')
|
||||
|| label.ends_with('-')
|
||||
|| !label
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"DNS readiness query name is invalid",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(query.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn is_effectively_allowed(address: [u8; 4], allow: &[Ipv4Prefix], deny: &[Ipv4Prefix]) -> bool {
|
||||
let best_allow = allow
|
||||
.iter()
|
||||
.filter(|prefix| prefix_contains(**prefix, address))
|
||||
.map(|prefix| prefix.prefix_length)
|
||||
.max();
|
||||
let best_deny = deny
|
||||
.iter()
|
||||
.filter(|prefix| prefix_contains(**prefix, address))
|
||||
.map(|prefix| prefix.prefix_length)
|
||||
.max();
|
||||
match (best_allow, best_deny) {
|
||||
(Some(allowed), Some(denied)) => allowed > denied,
|
||||
(Some(_) | None, None) => true,
|
||||
(None, Some(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_contains(prefix: Ipv4Prefix, address: [u8; 4]) -> bool {
|
||||
let mask = if prefix.prefix_length == 0 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << (32 - prefix.prefix_length)
|
||||
};
|
||||
u32::from_be_bytes(address) & mask == u32::from_be_bytes(prefix.address)
|
||||
}
|
||||
|
||||
pub(super) fn parse_ipv4_prefix(value: &str) -> Result<Ipv4Prefix, ConfigurationError> {
|
||||
let (address, prefix) = value.split_once('/').ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use super::confined_io::{
|
|||
host_path, io_error, is_strict_descendant, paths_intersect, read_stable_configuration,
|
||||
require_clear_of_protected, require_pairwise_disjoint, require_pairwise_non_ancestral,
|
||||
require_strict_child_of_one, resolve_environment_references, resolve_environment_values,
|
||||
resolve_network, resolve_network_readiness, validate_configuration_stat, validate_executable,
|
||||
validate_host_base, validate_persistent_destinations,
|
||||
resolve_network, validate_configuration_stat, validate_executable, validate_host_base,
|
||||
validate_persistent_destinations,
|
||||
};
|
||||
use super::validation::{
|
||||
validate_profile_name, validate_sandbox_path, validate_structural_configuration, validate_text,
|
||||
|
|
@ -153,11 +153,6 @@ pub fn load_trusted_configuration(
|
|||
)?;
|
||||
let (effective_allow_prefixes, effective_deny_prefixes) =
|
||||
resolve_network(&configuration, selected)?;
|
||||
let network_readiness = resolve_network_readiness(
|
||||
&selected.readiness,
|
||||
&effective_allow_prefixes,
|
||||
&effective_deny_prefixes,
|
||||
)?;
|
||||
let environment_references = resolve_environment_references(selected)?;
|
||||
|
||||
let mut volumes = Vec::with_capacity(selected.volumes.len());
|
||||
|
|
@ -360,7 +355,6 @@ pub fn load_trusted_configuration(
|
|||
.iter()
|
||||
.map(|prefix| (prefix.address, prefix.prefix_length))
|
||||
.collect(),
|
||||
network_readiness,
|
||||
escalation_enabled: selected.escalation.enabled,
|
||||
auto_approval_rules: resolved_auto_approval_records,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,34 +68,6 @@ pub fn derive_configuration_fingerprint(
|
|||
.iter()
|
||||
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.denied.iter().map(|target| {
|
||||
Ok([
|
||||
target.address.as_slice(),
|
||||
target.port.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat())
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.permitted.iter().map(|target| {
|
||||
Ok([
|
||||
target.address.as_slice(),
|
||||
target.port.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat())
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.dns.iter().map(|target| {
|
||||
let mut item = target.resolver.to_vec();
|
||||
push_bytes(&mut item, target.query.as_bytes())?;
|
||||
Ok(item)
|
||||
}),
|
||||
)?;
|
||||
push_bool(&mut preimage, record.escalation_enabled);
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
|
|
@ -128,6 +100,12 @@ pub(super) fn validate_structural_configuration(
|
|||
}
|
||||
for (sandbox_name, reference) in &profile.env {
|
||||
validate_environment_name(sandbox_name)?;
|
||||
if sandbox_name == "LAZARET_SESSION_ID" {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"environment name is reserved by Lazaret",
|
||||
));
|
||||
}
|
||||
let host_name = reference.strip_prefix("env:").ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
|
|
|
|||
Loading…
Reference in a new issue