Files
wasmeld/crates/wasmeld-runtime/src/resident_host.rs
T
Maofeng 572fed47b4 feat(runtime): supervise resident component resources
Detect the resident actor export, route Host events through the bounded Actor mailbox, enforce fuel, deadlines, payload limits, and translate WIT effects into typed runtime values.

Add a revision-scoped ResidentSession with deny-by-default TCP, UDP, and Unix endpoint policies, non-reused resource IDs, stream lifecycle and backpressure state, atomic effect validation, timers, subscriptions, extension sources, and shutdown handling.

Cover passive rejection, real Component ABI round trips, resource ownership, endpoint policy, and driver operation validation with integration tests.
2026-07-30 07:40:08 +08:00

1058 lines
34 KiB
Rust

//! Host-side ownership and state machine for resident Component resources.
//!
//! # Architecture
//!
//! A resident Component is not a native daemon and never owns a socket, file
//! descriptor, timer task, broker consumer, or async runtime. Those objects
//! belong to a Host driver. The driver translates external activity into
//! [`ResidentEvent`](crate::ResidentEvent) values and applies the validated
//! [`ResidentOperation`] values returned by this module.
//!
//! ```text
//! TCP / UDP / Unix / timer / broker / custom driver
//! |
//! v
//! ResidentSession
//! resource + state checks
//! |
//! v
//! bounded Actor mailbox
//! |
//! v
//! Wasm Component
//! |
//! raw effects
//! |
//! v
//! ResidentSession
//! ownership + policy checks
//! |
//! v
//! driver operations
//! ```
//!
//! One session is pinned to one [`ServiceKey`]. It must not be reused when a
//! deployment switches revisions. The deployment manager should start a new
//! Actor and session, attach new drivers, drain the old session, deliver
//! `shutdown`, close its Host resources, and finally stop the old Actor.
//!
//! Driver event loops may run concurrently, but calls into a session must be
//! serialized by their owner (normally one supervisor task). The Actor mailbox
//! is the final serialization boundary for Component memory.
use std::{
collections::HashMap,
fs,
net::SocketAddr,
path::{Component as PathComponent, Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
ActorHandle, ComponentExecution, ResidentEffect, ResidentEvent, ResourceId, RuntimeError,
ServiceKey, StreamCloseReason,
};
/// Network exposure allowed for one transport direction.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum NetworkScope {
/// No endpoint of this class may be registered.
#[default]
Disabled,
/// Only numeric loopback addresses are accepted.
Loopback,
/// Any numeric IP address is accepted.
Any,
}
impl NetworkScope {
fn allows(self, address: SocketAddr) -> bool {
match self {
Self::Disabled => false,
Self::Loopback => address.ip().is_loopback(),
Self::Any => true,
}
}
}
/// Platform policy applied before a Host driver binds an endpoint.
///
/// The default is deny-all. Unix roots must already exist so their canonical
/// paths can be checked before a socket is created. A Unix driver must perform
/// the same check again immediately before bind to prevent path replacement
/// between validation and use.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ResidentPolicy {
pub tcp_listen: NetworkScope,
pub udp_bind: NetworkScope,
pub unix_listen_roots: Vec<PathBuf>,
}
/// A management-defined endpoint. Components cannot create listeners.
///
/// Using `SocketAddr` instead of a hostname avoids DNS changes bypassing the
/// configured network scope. Protocols such as HTTP, WebSocket, gRPC, TLS, and
/// framed streams are implemented by Host drivers above these transports.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "transport", rename_all = "snake_case")]
pub enum ResidentEndpoint {
Tcp { name: String, bind: SocketAddr },
Udp { name: String, bind: SocketAddr },
Unix { name: String, path: PathBuf },
}
impl ResidentEndpoint {
fn name(&self) -> &str {
match self {
Self::Tcp { name, .. } | Self::Udp { name, .. } | Self::Unix { name, .. } => name,
}
}
}
/// Stable category exposed in management resource snapshots.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResidentResourceKind {
TcpEndpoint,
UdpEndpoint,
UnixEndpoint,
Stream,
Timer,
MessageSubscription,
Source,
}
/// Observable lifecycle state without exposing driver handles.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResidentResourceState {
Ready,
Open,
ReadPaused,
PeerHalfClosed,
Closing,
}
/// Management-safe snapshot of one Host-owned resource.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ResidentResourceInfo {
pub id: ResourceId,
pub name: String,
pub kind: ResidentResourceKind,
pub state: ResidentResourceState,
pub metadata: ResidentResourceMetadata,
}
/// Kind-specific metadata required to correlate a resource with its driver.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResidentResourceMetadata {
Endpoint { endpoint: ResidentEndpoint },
Stream { endpoint_id: ResourceId },
Timer { armed: bool },
MessageSubscription,
Source { source_kind: String },
}
/// An operation that passed payload, ownership, kind, and state validation.
///
/// This is the only output a network or source driver should execute. Raw
/// [`ResidentEffect`] values are intentionally kept inside [`ResidentSession`].
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResidentOperation {
WriteStream {
stream_id: ResourceId,
bytes: Vec<u8>,
},
CloseStream {
stream_id: ResourceId,
},
PauseStream {
stream_id: ResourceId,
},
ResumeStream {
stream_id: ResourceId,
},
SendDatagram {
endpoint_id: ResourceId,
peer: String,
bytes: Vec<u8>,
},
ArmTimer {
timer_id: ResourceId,
delay_ms: u64,
interval_ms: Option<u64>,
},
CancelTimer {
timer_id: ResourceId,
},
AcknowledgeMessage {
subscription_id: ResourceId,
message_id: u64,
},
SourceCommand {
source_id: ResourceId,
command: String,
payload: Vec<u8>,
},
}
/// Host resource, policy, or Actor failure while processing resident work.
#[derive(Debug, Error)]
pub enum ResidentHostError {
#[error(transparent)]
Runtime(#[from] RuntimeError),
#[error("resident session for {0} is shutting down")]
ShuttingDown(ServiceKey),
#[error("resident resource limit for {service} is {limit}")]
ResourceLimit { service: ServiceKey, limit: usize },
#[error("resident resource ids exhausted for {0}")]
ResourceIdsExhausted(ServiceKey),
#[error("resident resource name must not be empty")]
EmptyResourceName,
#[error("resident resource name {0} is already registered")]
DuplicateResourceName(String),
#[error("resident resource {resource_id} for {service} still owns active streams")]
ResourceInUse {
service: ServiceKey,
resource_id: u64,
},
#[error("resident resource {resource_id} does not belong to {service}")]
UnknownResource {
service: ServiceKey,
resource_id: u64,
},
#[error("resident resource {resource_id} for {service} is {actual:?}, expected {expected}")]
WrongResourceKind {
service: ServiceKey,
resource_id: u64,
actual: ResidentResourceKind,
expected: &'static str,
},
#[error("resident resource {resource_id} for {service} is in invalid state {state:?}")]
InvalidResourceState {
service: ServiceKey,
resource_id: u64,
state: ResidentResourceState,
},
#[error("resident endpoint denied by policy: {0}")]
EndpointDenied(String),
}
#[derive(Clone, Debug)]
enum ResourceEntry {
Endpoint(ResidentEndpoint),
Stream {
name: String,
endpoint_id: ResourceId,
state: StreamState,
},
Timer {
name: String,
armed: bool,
},
MessageSubscription {
name: String,
},
Source {
name: String,
source_kind: String,
},
}
impl ResourceEntry {
fn name(&self) -> &str {
match self {
Self::Endpoint(endpoint) => endpoint.name(),
Self::Stream { name, .. }
| Self::Timer { name, .. }
| Self::MessageSubscription { name }
| Self::Source { name, .. } => name,
}
}
fn kind(&self) -> ResidentResourceKind {
match self {
Self::Endpoint(ResidentEndpoint::Tcp { .. }) => ResidentResourceKind::TcpEndpoint,
Self::Endpoint(ResidentEndpoint::Udp { .. }) => ResidentResourceKind::UdpEndpoint,
Self::Endpoint(ResidentEndpoint::Unix { .. }) => ResidentResourceKind::UnixEndpoint,
Self::Stream { .. } => ResidentResourceKind::Stream,
Self::Timer { .. } => ResidentResourceKind::Timer,
Self::MessageSubscription { .. } => ResidentResourceKind::MessageSubscription,
Self::Source { .. } => ResidentResourceKind::Source,
}
}
fn state(&self) -> ResidentResourceState {
match self {
Self::Endpoint(_)
| Self::Timer { .. }
| Self::MessageSubscription { .. }
| Self::Source { .. } => ResidentResourceState::Ready,
Self::Stream { state, .. } => (*state).into(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StreamState {
Open,
ReadPaused,
PeerHalfClosed,
PeerHalfClosedReadPaused,
Closing,
}
impl From<StreamState> for ResidentResourceState {
fn from(state: StreamState) -> Self {
match state {
StreamState::Open => Self::Open,
StreamState::ReadPaused | StreamState::PeerHalfClosedReadPaused => Self::ReadPaused,
StreamState::PeerHalfClosed => Self::PeerHalfClosed,
StreamState::Closing => Self::Closing,
}
}
}
/// Revision-scoped supervisor for Host-owned resident resources.
///
/// This type intentionally performs no operating-system I/O. It makes driver
/// implementations replaceable and keeps Tokio or another async executor out
/// of the Wasmtime Actor thread.
pub struct ResidentSession {
actor: ActorHandle,
policy: ResidentPolicy,
resources: HashMap<ResourceId, ResourceEntry>,
next_resource_id: u64,
shutting_down: bool,
}
impl ResidentSession {
/// Creates a resource session for an Actor exporting resident WIT 0.1.0.
pub fn new(actor: ActorHandle, policy: ResidentPolicy) -> Result<Self, ResidentHostError> {
if actor.execution() != ComponentExecution::Resident {
return Err(RuntimeError::NotResident(actor.key().clone()).into());
}
Ok(Self {
actor,
policy,
resources: HashMap::new(),
next_resource_id: 1,
shutting_down: false,
})
}
/// Returns the immutable service revision that owns every resource ID.
pub fn owner(&self) -> &ServiceKey {
self.actor.key()
}
/// Registers a configured listener or datagram endpoint after policy checks.
pub fn register_endpoint(
&mut self,
endpoint: ResidentEndpoint,
) -> Result<ResourceId, ResidentHostError> {
self.ensure_active()?;
self.policy.validate_endpoint(&endpoint)?;
self.insert_resource(ResourceEntry::Endpoint(endpoint), true)
}
/// Registers a Host timer source. The driver decides its initial schedule.
pub fn register_timer(
&mut self,
name: impl Into<String>,
) -> Result<ResourceId, ResidentHostError> {
self.ensure_active()?;
self.insert_resource(
ResourceEntry::Timer {
name: name.into(),
armed: false,
},
true,
)
}
/// Registers a queue, broker, or internal mailbox subscription.
pub fn register_message_subscription(
&mut self,
name: impl Into<String>,
) -> Result<ResourceId, ResidentHostError> {
self.ensure_active()?;
self.insert_resource(
ResourceEntry::MessageSubscription { name: name.into() },
true,
)
}
/// Registers an extension driver such as a file watcher or serial device.
///
/// `source_kind` identifies the separately versioned capability contract
/// understood by both the driver and Component.
pub fn register_source(
&mut self,
name: impl Into<String>,
source_kind: impl Into<String>,
) -> Result<ResourceId, ResidentHostError> {
self.ensure_active()?;
let source_kind = source_kind.into();
if source_kind.trim().is_empty() {
return Err(ResidentHostError::EmptyResourceName);
}
self.insert_resource(
ResourceEntry::Source {
name: name.into(),
source_kind,
},
true,
)
}
/// Allocates a stream for a connection accepted by a TCP or Unix driver.
pub fn accept_stream(
&mut self,
endpoint_id: ResourceId,
peer: Option<String>,
) -> Result<(ResourceId, Vec<ResidentOperation>), ResidentHostError> {
self.ensure_active()?;
let endpoint = self.resource(endpoint_id)?;
match endpoint {
ResourceEntry::Endpoint(ResidentEndpoint::Tcp { .. })
| ResourceEntry::Endpoint(ResidentEndpoint::Unix { .. }) => {}
other => {
return Err(self.wrong_kind(endpoint_id, other.kind(), "TCP or Unix endpoint"));
}
}
let stream_id = self.insert_resource(
ResourceEntry::Stream {
name: format!("stream-{}", self.next_resource_id),
endpoint_id,
state: StreamState::Open,
},
false,
)?;
let result = self.dispatch(ResidentEvent::StreamOpened {
endpoint_id,
stream_id,
peer,
});
if result.is_err() {
self.resources.remove(&stream_id);
}
result.map(|operations| (stream_id, operations))
}
/// Delivers one bounded stream read. Paused and closing streams reject data.
pub fn stream_data(
&mut self,
stream_id: ResourceId,
bytes: Vec<u8>,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_stream_state(stream_id, &[StreamState::Open])?;
self.dispatch(ResidentEvent::StreamData { stream_id, bytes })
}
/// Signals that a driver can accept writes again.
pub fn stream_writable(
&mut self,
stream_id: ResourceId,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_stream_not_closing(stream_id)?;
self.dispatch(ResidentEvent::StreamWritable { stream_id })
}
/// Records a peer read-half close while preserving the local write half.
pub fn stream_half_closed(
&mut self,
stream_id: ResourceId,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
let state = self.stream_state_mut(stream_id)?;
*state = match *state {
StreamState::Open => StreamState::PeerHalfClosed,
StreamState::ReadPaused => StreamState::PeerHalfClosedReadPaused,
other => return Err(self.invalid_state(stream_id, other.into())),
};
self.dispatch(ResidentEvent::StreamHalfClosed { stream_id })
}
/// Delivers terminal stream state and releases the resource identity.
pub fn stream_closed(
&mut self,
stream_id: ResourceId,
reason: StreamCloseReason,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_kind(stream_id, ResidentResourceKind::Stream, "stream")?;
let result = self.dispatch(ResidentEvent::StreamClosed { stream_id, reason });
self.resources.remove(&stream_id);
result
}
/// Delivers one UDP datagram to its configured endpoint.
pub fn datagram(
&mut self,
endpoint_id: ResourceId,
peer: String,
bytes: Vec<u8>,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_kind(
endpoint_id,
ResidentResourceKind::UdpEndpoint,
"UDP endpoint",
)?;
self.dispatch(ResidentEvent::Datagram {
endpoint_id,
peer,
bytes,
})
}
/// Delivers a timer occurrence.
pub fn timer_fired(
&mut self,
timer_id: ResourceId,
scheduled_at_ns: u64,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_kind(timer_id, ResidentResourceKind::Timer, "timer")?;
if let ResourceEntry::Timer { armed, .. } = self.resource_mut(timer_id)? {
*armed = false;
}
self.dispatch(ResidentEvent::Timer {
timer_id,
scheduled_at_ns,
})
}
/// Delivers a broker or internal queue message.
pub fn message(
&mut self,
subscription_id: ResourceId,
message_id: u64,
bytes: Vec<u8>,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_kind(
subscription_id,
ResidentResourceKind::MessageSubscription,
"message subscription",
)?;
self.dispatch(ResidentEvent::Message {
subscription_id,
message_id,
bytes,
})
}
/// Delivers an event from a separately versioned extension driver.
pub fn source_event(
&mut self,
source_id: ResourceId,
event: String,
payload: Vec<u8>,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_kind(source_id, ResidentResourceKind::Source, "source")?;
self.dispatch(ResidentEvent::Source {
source_id,
event,
payload,
})
}
/// Gives the Component one final bounded callback and rejects later events.
///
/// After applying the returned operations, the owner closes every driver
/// resource and stops the Actor. Shutdown is not a drain barrier by itself.
pub fn shutdown(&mut self) -> Result<Vec<ResidentOperation>, ResidentHostError> {
self.ensure_active()?;
let result = self.dispatch(ResidentEvent::Shutdown);
self.shutting_down = true;
result
}
/// Removes a Host resource after its driver has released the actual handle.
pub fn release_resource(
&mut self,
resource_id: ResourceId,
) -> Result<ResidentResourceInfo, ResidentHostError> {
if self.resources.values().any(|entry| {
matches!(
entry,
ResourceEntry::Stream { endpoint_id, .. } if *endpoint_id == resource_id
)
}) {
return Err(ResidentHostError::ResourceInUse {
service: self.owner().clone(),
resource_id: resource_id.get(),
});
}
let entry = self
.resources
.remove(&resource_id)
.ok_or_else(|| self.unknown_resource(resource_id))?;
Ok(resource_info(resource_id, &entry))
}
/// Returns one management-safe resource snapshot.
pub fn resource_info(
&self,
resource_id: ResourceId,
) -> Result<ResidentResourceInfo, ResidentHostError> {
self.resource(resource_id)
.map(|entry| resource_info(resource_id, entry))
}
/// Returns all resource snapshots ordered by their non-reused IDs.
pub fn resources(&self) -> Vec<ResidentResourceInfo> {
let mut resources = self
.resources
.iter()
.map(|(id, entry)| resource_info(*id, entry))
.collect::<Vec<_>>();
resources.sort_unstable_by_key(|resource| resource.id);
resources
}
fn dispatch(
&mut self,
event: ResidentEvent,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
let effects = self.actor.dispatch_event(event)?;
self.apply_effects(effects)
}
fn apply_effects(
&mut self,
effects: Vec<ResidentEffect>,
) -> Result<Vec<ResidentOperation>, ResidentHostError> {
// Validate the whole batch against a copy. A bad later effect must not
// leave pause, close, or timer state from an earlier effect committed.
let mut next_resources = self.resources.clone();
let mut operations = Vec::with_capacity(effects.len());
for effect in effects {
operations.push(self.validate_effect(&mut next_resources, effect)?);
}
self.resources = next_resources;
Ok(operations)
}
fn validate_effect(
&self,
resources: &mut HashMap<ResourceId, ResourceEntry>,
effect: ResidentEffect,
) -> Result<ResidentOperation, ResidentHostError> {
match effect {
ResidentEffect::WriteStream { stream_id, bytes } => {
ensure_stream_not_closing(resources, self.owner(), stream_id)?;
Ok(ResidentOperation::WriteStream { stream_id, bytes })
}
ResidentEffect::CloseStream { stream_id } => {
*stream_state_mut(resources, self.owner(), stream_id)? = StreamState::Closing;
Ok(ResidentOperation::CloseStream { stream_id })
}
ResidentEffect::PauseStream { stream_id } => {
let state = stream_state_mut(resources, self.owner(), stream_id)?;
*state = match *state {
StreamState::Open => StreamState::ReadPaused,
StreamState::PeerHalfClosed => StreamState::PeerHalfClosedReadPaused,
other => return Err(invalid_state(self.owner(), stream_id, other.into())),
};
Ok(ResidentOperation::PauseStream { stream_id })
}
ResidentEffect::ResumeStream { stream_id } => {
let state = stream_state_mut(resources, self.owner(), stream_id)?;
*state = match *state {
StreamState::ReadPaused => StreamState::Open,
StreamState::PeerHalfClosedReadPaused => StreamState::PeerHalfClosed,
other => return Err(invalid_state(self.owner(), stream_id, other.into())),
};
Ok(ResidentOperation::ResumeStream { stream_id })
}
ResidentEffect::SendDatagram {
endpoint_id,
peer,
bytes,
} => {
ensure_kind(
resources,
self.owner(),
endpoint_id,
ResidentResourceKind::UdpEndpoint,
"UDP endpoint",
)?;
Ok(ResidentOperation::SendDatagram {
endpoint_id,
peer,
bytes,
})
}
ResidentEffect::ArmTimer {
timer_id,
delay_ms,
interval_ms,
} => {
let entry = resource_mut(resources, self.owner(), timer_id)?;
let ResourceEntry::Timer { armed, .. } = entry else {
return Err(wrong_kind(self.owner(), timer_id, entry.kind(), "timer"));
};
*armed = true;
Ok(ResidentOperation::ArmTimer {
timer_id,
delay_ms,
interval_ms,
})
}
ResidentEffect::CancelTimer { timer_id } => {
let entry = resource_mut(resources, self.owner(), timer_id)?;
let ResourceEntry::Timer { armed, .. } = entry else {
return Err(wrong_kind(self.owner(), timer_id, entry.kind(), "timer"));
};
*armed = false;
Ok(ResidentOperation::CancelTimer { timer_id })
}
ResidentEffect::AcknowledgeMessage {
subscription_id,
message_id,
} => {
ensure_kind(
resources,
self.owner(),
subscription_id,
ResidentResourceKind::MessageSubscription,
"message subscription",
)?;
Ok(ResidentOperation::AcknowledgeMessage {
subscription_id,
message_id,
})
}
ResidentEffect::SourceCommand {
source_id,
command,
payload,
} => {
ensure_kind(
resources,
self.owner(),
source_id,
ResidentResourceKind::Source,
"source",
)?;
Ok(ResidentOperation::SourceCommand {
source_id,
command,
payload,
})
}
}
}
fn insert_resource(
&mut self,
entry: ResourceEntry,
unique_name: bool,
) -> Result<ResourceId, ResidentHostError> {
if entry.name().trim().is_empty() {
return Err(ResidentHostError::EmptyResourceName);
}
if unique_name
&& self
.resources
.values()
.any(|existing| existing.name() == entry.name())
{
return Err(ResidentHostError::DuplicateResourceName(
entry.name().to_owned(),
));
}
let limit = self.actor.resident_resource_limit();
if self.resources.len() >= limit {
return Err(ResidentHostError::ResourceLimit {
service: self.owner().clone(),
limit,
});
}
let id = ResourceId::new(self.next_resource_id)
.ok_or_else(|| ResidentHostError::ResourceIdsExhausted(self.owner().clone()))?;
self.next_resource_id = self
.next_resource_id
.checked_add(1)
.ok_or_else(|| ResidentHostError::ResourceIdsExhausted(self.owner().clone()))?;
self.resources.insert(id, entry);
Ok(id)
}
fn ensure_active(&self) -> Result<(), ResidentHostError> {
if self.shutting_down {
Err(ResidentHostError::ShuttingDown(self.owner().clone()))
} else {
Ok(())
}
}
fn resource(&self, id: ResourceId) -> Result<&ResourceEntry, ResidentHostError> {
self.resources
.get(&id)
.ok_or_else(|| self.unknown_resource(id))
}
fn resource_mut(&mut self, id: ResourceId) -> Result<&mut ResourceEntry, ResidentHostError> {
let owner = self.owner().clone();
self.resources
.get_mut(&id)
.ok_or(ResidentHostError::UnknownResource {
service: owner,
resource_id: id.get(),
})
}
fn ensure_kind(
&self,
id: ResourceId,
expected_kind: ResidentResourceKind,
expected: &'static str,
) -> Result<(), ResidentHostError> {
let entry = self.resource(id)?;
if entry.kind() == expected_kind {
Ok(())
} else {
Err(self.wrong_kind(id, entry.kind(), expected))
}
}
fn stream_state_mut(&mut self, id: ResourceId) -> Result<&mut StreamState, ResidentHostError> {
let owner = self.owner().clone();
stream_state_mut(&mut self.resources, &owner, id)
}
fn ensure_stream_state(
&self,
id: ResourceId,
allowed: &[StreamState],
) -> Result<(), ResidentHostError> {
let entry = self.resource(id)?;
let ResourceEntry::Stream { state, .. } = entry else {
return Err(self.wrong_kind(id, entry.kind(), "stream"));
};
if allowed.contains(state) {
Ok(())
} else {
Err(self.invalid_state(id, (*state).into()))
}
}
fn ensure_stream_not_closing(&self, id: ResourceId) -> Result<(), ResidentHostError> {
ensure_stream_not_closing(&self.resources, self.owner(), id)
}
fn unknown_resource(&self, id: ResourceId) -> ResidentHostError {
ResidentHostError::UnknownResource {
service: self.owner().clone(),
resource_id: id.get(),
}
}
fn wrong_kind(
&self,
id: ResourceId,
actual: ResidentResourceKind,
expected: &'static str,
) -> ResidentHostError {
wrong_kind(self.owner(), id, actual, expected)
}
fn invalid_state(&self, id: ResourceId, state: ResidentResourceState) -> ResidentHostError {
invalid_state(self.owner(), id, state)
}
}
impl ResidentPolicy {
fn validate_endpoint(&self, endpoint: &ResidentEndpoint) -> Result<(), ResidentHostError> {
match endpoint {
ResidentEndpoint::Tcp { bind, .. } if !self.tcp_listen.allows(*bind) => {
Err(ResidentHostError::EndpointDenied(format!(
"TCP listen address {bind} is outside {:?}",
self.tcp_listen
)))
}
ResidentEndpoint::Udp { bind, .. } if !self.udp_bind.allows(*bind) => {
Err(ResidentHostError::EndpointDenied(format!(
"UDP bind address {bind} is outside {:?}",
self.udp_bind
)))
}
ResidentEndpoint::Unix { path, .. } => self.validate_unix_path(path),
ResidentEndpoint::Tcp { .. } | ResidentEndpoint::Udp { .. } => Ok(()),
}
}
fn validate_unix_path(&self, path: &Path) -> Result<(), ResidentHostError> {
if !path.is_absolute()
|| path
.components()
.any(|component| matches!(component, PathComponent::ParentDir))
{
return Err(ResidentHostError::EndpointDenied(format!(
"Unix socket path {} must be absolute and contain no parent traversal",
path.display()
)));
}
let parent = path.parent().ok_or_else(|| {
ResidentHostError::EndpointDenied(format!(
"Unix socket path {} has no parent",
path.display()
))
})?;
let resolved_parent = fs::canonicalize(parent).map_err(|error| {
ResidentHostError::EndpointDenied(format!(
"Unix socket parent {} cannot be resolved: {error}",
parent.display()
))
})?;
for root in &self.unix_listen_roots {
let resolved_root = fs::canonicalize(root).map_err(|error| {
ResidentHostError::EndpointDenied(format!(
"configured Unix root {} cannot be resolved: {error}",
root.display()
))
})?;
if resolved_parent.starts_with(resolved_root) {
return Ok(());
}
}
Err(ResidentHostError::EndpointDenied(format!(
"Unix socket path {} is outside configured roots",
path.display()
)))
}
}
fn resource_info(id: ResourceId, entry: &ResourceEntry) -> ResidentResourceInfo {
ResidentResourceInfo {
id,
name: entry.name().to_owned(),
kind: entry.kind(),
state: entry.state(),
metadata: match entry {
ResourceEntry::Endpoint(endpoint) => ResidentResourceMetadata::Endpoint {
endpoint: endpoint.clone(),
},
ResourceEntry::Stream { endpoint_id, .. } => ResidentResourceMetadata::Stream {
endpoint_id: *endpoint_id,
},
ResourceEntry::Timer { armed, .. } => ResidentResourceMetadata::Timer { armed: *armed },
ResourceEntry::MessageSubscription { .. } => {
ResidentResourceMetadata::MessageSubscription
}
ResourceEntry::Source { source_kind, .. } => ResidentResourceMetadata::Source {
source_kind: source_kind.clone(),
},
},
}
}
fn resource_mut<'a>(
resources: &'a mut HashMap<ResourceId, ResourceEntry>,
owner: &ServiceKey,
id: ResourceId,
) -> Result<&'a mut ResourceEntry, ResidentHostError> {
resources
.get_mut(&id)
.ok_or_else(|| ResidentHostError::UnknownResource {
service: owner.clone(),
resource_id: id.get(),
})
}
fn stream_state_mut<'a>(
resources: &'a mut HashMap<ResourceId, ResourceEntry>,
owner: &ServiceKey,
id: ResourceId,
) -> Result<&'a mut StreamState, ResidentHostError> {
let entry = resource_mut(resources, owner, id)?;
let ResourceEntry::Stream { state, .. } = entry else {
return Err(wrong_kind(owner, id, entry.kind(), "stream"));
};
Ok(state)
}
fn ensure_stream_not_closing(
resources: &HashMap<ResourceId, ResourceEntry>,
owner: &ServiceKey,
id: ResourceId,
) -> Result<(), ResidentHostError> {
let entry = resources
.get(&id)
.ok_or_else(|| ResidentHostError::UnknownResource {
service: owner.clone(),
resource_id: id.get(),
})?;
let ResourceEntry::Stream { state, .. } = entry else {
return Err(wrong_kind(owner, id, entry.kind(), "stream"));
};
if *state == StreamState::Closing {
Err(invalid_state(owner, id, (*state).into()))
} else {
Ok(())
}
}
fn ensure_kind(
resources: &HashMap<ResourceId, ResourceEntry>,
owner: &ServiceKey,
id: ResourceId,
expected_kind: ResidentResourceKind,
expected: &'static str,
) -> Result<(), ResidentHostError> {
let entry = resources
.get(&id)
.ok_or_else(|| ResidentHostError::UnknownResource {
service: owner.clone(),
resource_id: id.get(),
})?;
if entry.kind() == expected_kind {
Ok(())
} else {
Err(wrong_kind(owner, id, entry.kind(), expected))
}
}
fn wrong_kind(
owner: &ServiceKey,
id: ResourceId,
actual: ResidentResourceKind,
expected: &'static str,
) -> ResidentHostError {
ResidentHostError::WrongResourceKind {
service: owner.clone(),
resource_id: id.get(),
actual,
expected,
}
}
fn invalid_state(
owner: &ServiceKey,
id: ResourceId,
state: ResidentResourceState,
) -> ResidentHostError {
ResidentHostError::InvalidResourceState {
service: owner.clone(),
resource_id: id.get(),
state,
}
}