Replace external reachability with an owned canary

This commit is contained in:
ms2k 2026-07-19 22:39:31 +09:00
commit 31e9ab0f2d
3 changed files with 1409 additions and 1972 deletions

View file

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

View file

@ -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,
};

View file

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