2026-05-31 08:58:08 -06:00
|
|
|
use crate::protocol::{Endpoint, EndpointError, EndpointName};
|
2026-05-28 14:46:47 -06:00
|
|
|
|
|
|
|
|
/// Compact identifier for one routed return channel.
|
|
|
|
|
///
|
|
|
|
|
/// Hook ids are local endpoint state, not globally unique session ids. A downward
|
|
|
|
|
/// packet with `end_hook = false` reserves the id at each endpoint it crosses so
|
|
|
|
|
/// later upward packets can prove that the route was paved by trusted downward
|
|
|
|
|
/// traffic first.
|
|
|
|
|
pub type HookID = u16;
|
|
|
|
|
|
|
|
|
|
impl Endpoint {
|
|
|
|
|
/// Allocates a hook id that is not currently active on this endpoint.
|
|
|
|
|
///
|
|
|
|
|
/// The first id is still deterministic (`0`) for the protocol tests, but the
|
|
|
|
|
/// allocator now skips active hooks so long-lived streams cannot accidentally
|
|
|
|
|
/// reuse an id before the previous route has closed. If every `u16` id is active
|
|
|
|
|
/// the function panics; that is a hard local resource exhaustion condition, not a
|
|
|
|
|
/// recoverable packet error.
|
2026-05-31 14:47:25 -06:00
|
|
|
///
|
|
|
|
|
/// TODO: Reevaluate this method of allocation checking. It can be quite slow
|
2026-05-28 14:46:47 -06:00
|
|
|
pub fn allocate_hook_id(&mut self) -> HookID {
|
|
|
|
|
for _ in 0..=HookID::MAX {
|
2026-05-31 14:47:25 -06:00
|
|
|
let candidate = self.last_hook.next();
|
2026-05-28 14:46:47 -06:00
|
|
|
|
|
|
|
|
if !self.hooks.contains_key(&candidate) {
|
|
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Avoid a panic message here: this crate is optimized for small binaries,
|
|
|
|
|
// and exhausting every `u16` hook id is unrecoverable local state corruption.
|
|
|
|
|
panic!();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Backwards-compatible name for [`Self::allocate_hook_id`].
|
|
|
|
|
///
|
|
|
|
|
/// Existing leaves and tests still call `get_hook_id`; new code should prefer
|
|
|
|
|
/// `allocate_hook_id` because it describes the reservation semantics more clearly.
|
|
|
|
|
pub fn get_hook_id(&mut self) -> HookID {
|
|
|
|
|
self.allocate_hook_id()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Explicitly records that `peer` may use `hook_id` as this endpoint's return channel.
|
|
|
|
|
///
|
|
|
|
|
/// Routing calls this automatically for successful downward packets whose
|
|
|
|
|
/// `end_hook` flag is false. The public method exists for trusted local setup and
|
|
|
|
|
/// tests; ordinary leaf procedures should usually let packet routing pave hooks
|
|
|
|
|
/// instead of mutating hook state by hand.
|
|
|
|
|
pub fn accept_hook(&mut self, hook_id: HookID, peer: u32) -> Option<u32> {
|
|
|
|
|
self.hooks.insert(hook_id, peer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns true when `hook_id` is currently active.
|
|
|
|
|
pub fn has_hook(&self, hook_id: HookID) -> bool {
|
|
|
|
|
self.hooks.contains_key(&hook_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the adjacent peer currently associated with `hook_id`.
|
|
|
|
|
///
|
|
|
|
|
/// The peer is the next endpoint expected to participate in the return channel:
|
|
|
|
|
/// a child for downward calls that will reply upward, or a parent for a local
|
|
|
|
|
/// callee that will emit an upward response.
|
|
|
|
|
pub fn hook_peer(&self, hook_id: HookID) -> Option<u32> {
|
|
|
|
|
self.hooks.get(&hook_id).copied()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the number of active hooks on this endpoint.
|
|
|
|
|
pub fn hook_count(&self) -> usize {
|
|
|
|
|
self.hooks.len()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Locally forgets a hook without sending protocol traffic.
|
|
|
|
|
///
|
|
|
|
|
/// Graceful shutdown should use a packet with `end_hook = true` so every endpoint
|
|
|
|
|
/// along the route cleans up after successful delivery. This method is for local
|
|
|
|
|
/// emergency cleanup such as a crashed PTY process, a timed-out stream, or a lost
|
|
|
|
|
/// transport where no final packet can be delivered.
|
|
|
|
|
pub fn forget_hook(&mut self, hook_id: HookID) -> bool {
|
|
|
|
|
self.close_hook(hook_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validates that `actual_peer` is the peer allowed to use `hook_id`.
|
|
|
|
|
pub(crate) fn ensure_hook_peer(
|
|
|
|
|
&self,
|
|
|
|
|
hook_id: HookID,
|
|
|
|
|
actual_peer: EndpointName,
|
|
|
|
|
) -> Result<(), EndpointError> {
|
|
|
|
|
let expected_peer = self
|
|
|
|
|
.hook_peer(hook_id)
|
|
|
|
|
.ok_or(EndpointError::UnknownHook { hook_id })?;
|
|
|
|
|
|
|
|
|
|
if expected_peer == actual_peer {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(EndpointError::HookPeerMismatch {
|
|
|
|
|
hook_id,
|
|
|
|
|
expected_peer,
|
|
|
|
|
actual_peer,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Opens or refreshes `hook_id` for the adjacent `peer` after downward routing succeeds.
|
|
|
|
|
pub(crate) fn open_hook(&mut self, hook_id: HookID, peer: EndpointName) {
|
|
|
|
|
self.hooks.insert(hook_id, peer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Removes `hook_id` and reports whether it existed.
|
|
|
|
|
pub(crate) fn close_hook(&mut self, hook_id: HookID) -> bool {
|
|
|
|
|
self.hooks.remove(&hook_id).is_some()
|
|
|
|
|
}
|
|
|
|
|
}
|