Derive session routing from hooks

This commit is contained in:
Michael Mikovsky
2026-06-01 11:18:16 -06:00
parent 8a817cb5eb
commit 8ab72d35b0
7 changed files with 130 additions and 58 deletions
Generated
+7
View File
@@ -829,6 +829,13 @@ dependencies = [
"unshell", "unshell",
] ]
[[package]]
name = "leaf-shell"
version = "0.1.0"
dependencies = [
"unshell",
]
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
version = "0.1.0" version = "0.1.0"
+1 -1
View File
@@ -5,7 +5,7 @@ members = [
"ush-obfuscate", "ush-obfuscate",
"base62", "base62",
"unshell-leaves/leaf-pty", "unshell-leaves/leaf-pty", "unshell-leaves/leaf-shell",
] ]
resolver = "2" resolver = "2"
+7 -9
View File
@@ -1,5 +1,3 @@
use alloc::string::String;
// TODO: Make this seed dependent on env var; // TODO: Make this seed dependent on env var;
pub const GLOBAL_SEED: u32 = 0xDEAFBEEF; pub const GLOBAL_SEED: u32 = 0xDEAFBEEF;
// pub const GLOBAL_NONCE: u32 = { // pub const GLOBAL_NONCE: u32 = {
@@ -55,17 +53,17 @@ macro_rules! hash_32 {
}}; }};
} }
pub fn hash_string_32(input: String) -> u32 { // pub const fn hash_string_32(input: String) -> u32 {
// let hash: [u8; 32] = sha256(input.as_bytes());
// u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]])
// }
pub const fn hash_str_32(input: &str) -> u32 {
let hash: [u8; 32] = sha256(input.as_bytes()); let hash: [u8; 32] = sha256(input.as_bytes());
u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]]) u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]])
} }
pub fn hash_str_32(input: &str) -> u32 { pub const fn hash_32(input: u32) -> u32 {
let hash: [u8; 32] = sha256(input.as_bytes());
u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]])
}
pub fn hash_32(input: u32) -> u32 {
let hash: [u8; 32] = sha256(&input.to_be_bytes()); let hash: [u8; 32] = sha256(&input.to_be_bytes());
u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]]) u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]])
} }
+26
View File
@@ -1,3 +1,5 @@
use alloc::vec::Vec;
use crate::protocol::{Endpoint, EndpointError, EndpointName}; use crate::protocol::{Endpoint, EndpointError, EndpointName};
/// Compact identifier for one routed return channel. /// Compact identifier for one routed return channel.
@@ -79,6 +81,30 @@ impl Endpoint {
self.close_hook(hook_id) self.close_hook(hook_id)
} }
/// Returns the destination path for packets sent back over `hook_id`.
///
/// Hooks record the adjacent peer that paved the return channel. This helper turns
/// that peer into the packet path required by the current router: parent peers map
/// to the parent path, and child peers map to the direct child path. Session logic
/// should not store this path itself.
pub(crate) fn hook_path(&self, hook_id: HookID) -> Result<Vec<u32>, EndpointError> {
let peer = self
.hook_peer(hook_id)
.ok_or(EndpointError::UnknownHook { hook_id })?;
if self.path.is_empty() {
return Err(EndpointError::EndpointPathUnset);
}
if self.path.len() > 1 && self.path[self.path.len() - 2] == peer {
Ok(self.path[..self.path.len() - 1].to_vec())
} else {
let mut path = self.path.clone();
path.push(peer);
Ok(path)
}
}
/// Validates that `actual_peer` is the peer allowed to use `hook_id`. /// Validates that `actual_peer` is the peer allowed to use `hook_id`.
pub(crate) fn ensure_hook_peer( pub(crate) fn ensure_hook_peer(
&self, &self,
+1
View File
@@ -123,6 +123,7 @@ macro_rules! unshell_leaf {
== <$Session as $crate::protocol::Session<$State>>::PROCEDURE_ID == <$Session as $crate::protocol::Session<$State>>::PROCEDURE_ID
{ {
$crate::protocol::dispatch_session::<$State, $Session>( $crate::protocol::dispatch_session::<$State, $Session>(
endpoint,
leaf_id, leaf_id,
&mut self.state, &mut self.state,
&mut self.$session_field, &mut self.$session_field,
+31 -12
View File
@@ -4,7 +4,7 @@ use crate::{
interface::{InterfaceEventKind, InterfaceStore, InterfaceTarget}, interface::{InterfaceEventKind, InterfaceStore, InterfaceTarget},
protocol::{ protocol::{
Endpoint, Packet, PacketQueue, Procedure, ProcedureOut, Session, SessionCtx, SessionEntry, Endpoint, Packet, PacketQueue, Procedure, ProcedureOut, Session, SessionCtx, SessionEntry,
SessionFamily, SessionInit, SessionInitResult, SessionStatus, SessionFamily, SessionInit, SessionInitError, SessionStatus,
}, },
}; };
@@ -88,6 +88,7 @@ impl Default for LeafOutbox {
/// find the hook, initialize missing sessions, queue rejected responses, and update /// find the hook, initialize missing sessions, queue rejected responses, and update
/// interface state when a caller supplied one. /// interface state when a caller supplied one.
pub fn dispatch_session<L, S>( pub fn dispatch_session<L, S>(
endpoint: &Endpoint,
leaf_id: u32, leaf_id: u32,
leaf: &mut L, leaf: &mut L,
family: &mut SessionFamily<S::State>, family: &mut SessionFamily<S::State>,
@@ -131,12 +132,27 @@ pub fn dispatch_session<L, S>(
} }
let started_ns = interface.as_ref().and_then(|store| store.now_ns()); let started_ns = interface.as_ref().and_then(|store| store.now_ns());
let Ok(path) = endpoint.hook_path(hook_id) else {
if let Some(store) = interface.as_mut() {
store.record_for(
target,
InterfaceEventKind::SessionRejected {
procedure_id,
hook_id,
started_ns,
finished_ns: store.now_ns(),
},
);
}
return;
};
let packet_path = packet.path.clone(); let packet_path = packet.path.clone();
let mut init = SessionInit::new(hook_id, packet_path); let mut init = SessionInit::new(hook_id, packet_path);
match S::init(leaf, packet, &mut init) { match S::init(leaf, packet, &mut init) {
SessionInitResult::Created(state) => { Ok(state) => {
family.entries.push(SessionEntry::new(hook_id, state)); family.entries.push(SessionEntry::new(hook_id, path, state));
if let Some(store) = interface.as_mut() { if let Some(store) = interface.as_mut() {
store.record_for( store.record_for(
@@ -150,7 +166,7 @@ pub fn dispatch_session<L, S>(
); );
} }
} }
SessionInitResult::Rejected => { Err(SessionInitError::Rejected) => {
if let Some(store) = interface.as_mut() { if let Some(store) = interface.as_mut() {
store.record_for( store.record_for(
target, target,
@@ -163,7 +179,15 @@ pub fn dispatch_session<L, S>(
); );
} }
} }
SessionInitResult::RejectedWith(packet) => { Err(SessionInitError::Response { data, end_hook }) => {
let packet = Packet {
hook_id,
end_hook,
path,
procedure_id,
data,
};
if let Some(store) = interface.as_mut() { if let Some(store) = interface.as_mut() {
store.record_for( store.record_for(
target, target,
@@ -203,14 +227,9 @@ pub fn update_session_family<L, S>(
let started_ns = interface.as_ref().and_then(|store| store.now_ns()); let started_ns = interface.as_ref().and_then(|store| store.now_ns());
let outbox_start = entry.outbox.len(); let outbox_start = entry.outbox.len();
let reply_path = S::reply_path(&entry.state).to_vec(); let path = entry.path.clone();
let status = { let status = {
let mut ctx = SessionCtx::new( let mut ctx = SessionCtx::new(entry.hook_id, path, S::PROCEDURE_ID, &mut entry.outbox);
entry.hook_id,
reply_path,
S::PROCEDURE_ID,
&mut entry.outbox,
);
S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx) S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx)
}; };
+57 -36
View File
@@ -18,16 +18,12 @@ use crate::interface::SessionView;
/// const PROCEDURE_ID: u32 = 7; /// const PROCEDURE_ID: u32 = 7;
/// type State = MySessionState; /// type State = MySessionState;
/// ///
/// fn reply_path(state: &Self::State) -> &[u32] {
/// &state.reply_path
/// }
///
/// fn init( /// fn init(
/// leaf: &mut MyLeafState, /// leaf: &mut MyLeafState,
/// packet: Packet, /// packet: Packet,
/// ctx: &mut SessionInit, /// ctx: &mut SessionInit,
/// ) -> SessionInitResult<Self::State> { /// ) -> Result<Self::State, SessionInitError> {
/// SessionInitResult::Created(MySessionState::from_open(leaf, packet, ctx)) /// Ok(MySessionState::from_open(leaf, packet, ctx))
/// } /// }
/// ///
/// fn update( /// fn update(
@@ -50,20 +46,16 @@ pub trait Session<L> {
/// Application state stored for one live hook. /// Application state stored for one live hook.
type State; type State;
/// Returns the destination path for responses emitted by this session.
///
/// `Packet` currently carries only a destination path, so protocols that need to
/// reply to a caller should capture a reply path during [`Self::init`]. The
/// generated leaf clones this path into [`SessionCtx`] before calling update so
/// session code can mutably borrow its state while emitting frames.
fn reply_path(session: &Self::State) -> &[u32];
/// Creates one session state from a packet whose hook has no active session. /// Creates one session state from a packet whose hook has no active session.
/// ///
/// Returning [`SessionInitResult::RejectedWith`] lets the generated leaf route a /// The generated runtime derives all response routing from hook state. Session
/// protocol-level failure response with the same retry guarantees as normal /// initialization therefore returns only application state or a protocol-level
/// output. Returning [`SessionInitResult::Rejected`] silently consumes the packet. /// rejection; it never stores or receives a caller reply path.
fn init(leaf: &mut L, packet: Packet, ctx: &mut SessionInit) -> SessionInitResult<Self::State>; fn init(
leaf: &mut L,
packet: Packet,
ctx: &mut SessionInit,
) -> Result<Self::State, SessionInitError>;
/// Advances one active hook session. /// Advances one active hook session.
/// ///
@@ -119,16 +111,42 @@ impl SessionInit {
} }
} }
/// Result of trying to create a session from a packet without an active hook entry. /// Error returned when a packet cannot create a new session.
pub enum SessionInitResult<S> { pub enum SessionInitError {
/// A new session was created and should be stored by the generated leaf. /// The packet was intentionally consumed without creating state or sending output.
Created(S),
/// The packet was intentionally consumed without creating state or a response.
Rejected, Rejected,
/// The packet was rejected with a response that the generated leaf must route. /// The packet was rejected with response data that should be sent on the same hook.
RejectedWith(Packet), Response {
/// Raw `Packet::data` for the response frame.
data: Vec<u8>,
/// Whether the response should close the hook after successful routing.
end_hook: bool,
},
}
impl SessionInitError {
/// Creates a silent session rejection.
pub fn rejected() -> Self {
Self::Rejected
}
/// Creates a non-final response for a rejected session open.
pub fn response(data: Vec<u8>) -> Self {
Self::Response {
data,
end_hook: false,
}
}
/// Creates a final response for a rejected session open.
pub fn response_final(data: Vec<u8>) -> Self {
Self::Response {
data,
end_hook: true,
}
}
} }
/// Session lifecycle status returned from [`Session::update`]. /// Session lifecycle status returned from [`Session::update`].
@@ -153,7 +171,7 @@ pub enum SessionStatus {
/// routing in generated code is what makes final-frame retries reliable. /// routing in generated code is what makes final-frame retries reliable.
pub struct SessionCtx<'a> { pub struct SessionCtx<'a> {
hook_id: HookID, hook_id: HookID,
reply_path: Vec<u32>, path: Vec<u32>,
procedure_id: u32, procedure_id: u32,
outbox: &'a mut PacketQueue, outbox: &'a mut PacketQueue,
} }
@@ -162,13 +180,13 @@ impl<'a> SessionCtx<'a> {
/// Creates a context for one session update call. /// Creates a context for one session update call.
pub fn new( pub fn new(
hook_id: HookID, hook_id: HookID,
reply_path: Vec<u32>, path: Vec<u32>,
procedure_id: u32, procedure_id: u32,
outbox: &'a mut PacketQueue, outbox: &'a mut PacketQueue,
) -> Self { ) -> Self {
Self { Self {
hook_id, hook_id,
reply_path, path,
procedure_id, procedure_id,
outbox, outbox,
} }
@@ -179,11 +197,6 @@ impl<'a> SessionCtx<'a> {
self.hook_id self.hook_id
} }
/// Returns the destination path used for packets emitted through this context.
pub fn reply_path(&self) -> &[u32] {
&self.reply_path
}
/// Queues a one-byte-opcode frame without closing the hook. /// Queues a one-byte-opcode frame without closing the hook.
pub fn send(&mut self, opcode: u8, data: &[u8]) { pub fn send(&mut self, opcode: u8, data: &[u8]) {
self.send_frame(opcode, data, false); self.send_frame(opcode, data, false);
@@ -233,7 +246,7 @@ impl<'a> SessionCtx<'a> {
self.outbox.push_back(Packet { self.outbox.push_back(Packet {
hook_id: self.hook_id, hook_id: self.hook_id,
end_hook, end_hook,
path: self.reply_path.clone(), path: self.path.clone(),
procedure_id: self.procedure_id, procedure_id: self.procedure_id,
data, data,
}); });
@@ -249,6 +262,13 @@ pub struct SessionEntry<S> {
/// Hook id associated with this live session. /// Hook id associated with this live session.
pub hook_id: HookID, pub hook_id: HookID,
/// Destination path for packets emitted on this hook.
///
/// This is generated runtime state, not user session state. It is captured from
/// endpoint hook routing when the session is created so leaf sessions never have
/// to carry or understand a reply path.
pub path: Vec<u32>,
/// Application-owned session state. /// Application-owned session state.
pub state: S, pub state: S,
@@ -300,9 +320,10 @@ impl<S> Default for SessionFamily<S> {
impl<S> SessionEntry<S> { impl<S> SessionEntry<S> {
/// Creates one active session entry for `hook_id`. /// Creates one active session entry for `hook_id`.
pub fn new(hook_id: HookID, state: S) -> Self { pub fn new(hook_id: HookID, path: Vec<u32>, state: S) -> Self {
Self { Self {
hook_id, hook_id,
path,
state, state,
inbox: PacketQueue::new(), inbox: PacketQueue::new(),
outbox: PacketQueue::new(), outbox: PacketQueue::new(),