From 8ab72d35b0948d1505965303d9d15a46d7e361d0 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:18:16 -0600 Subject: [PATCH] Derive session routing from hooks --- Cargo.lock | 7 +++ Cargo.toml | 2 +- src/crypto/mod.rs | 16 +++--- src/protocol/endpoint/hooks.rs | 26 ++++++++++ src/protocol/leaf_template.rs | 1 + src/protocol/runtime.rs | 43 +++++++++++----- src/protocol/session.rs | 93 +++++++++++++++++++++------------- 7 files changed, 130 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb7df14..decd17c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -829,6 +829,13 @@ dependencies = [ "unshell", ] +[[package]] +name = "leaf-shell" +version = "0.1.0" +dependencies = [ + "unshell", +] + [[package]] name = "leb128fmt" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ef2c4bf..5452c78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [ "ush-obfuscate", "base62", - "unshell-leaves/leaf-pty", + "unshell-leaves/leaf-pty", "unshell-leaves/leaf-shell", ] resolver = "2" diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index beb35ca..684af3f 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,5 +1,3 @@ -use alloc::string::String; - // TODO: Make this seed dependent on env var; pub const GLOBAL_SEED: u32 = 0xDEAFBEEF; // 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()); u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]]) } -pub fn hash_str_32(input: &str) -> 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 { +pub const fn hash_32(input: u32) -> u32 { let hash: [u8; 32] = sha256(&input.to_be_bytes()); u32::from_be_bytes([hash[0], hash[8], hash[16], hash[24]]) } diff --git a/src/protocol/endpoint/hooks.rs b/src/protocol/endpoint/hooks.rs index 0b4f977..d2e1b8a 100644 --- a/src/protocol/endpoint/hooks.rs +++ b/src/protocol/endpoint/hooks.rs @@ -1,3 +1,5 @@ +use alloc::vec::Vec; + use crate::protocol::{Endpoint, EndpointError, EndpointName}; /// Compact identifier for one routed return channel. @@ -79,6 +81,30 @@ impl Endpoint { 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, 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`. pub(crate) fn ensure_hook_peer( &self, diff --git a/src/protocol/leaf_template.rs b/src/protocol/leaf_template.rs index cd54fde..339065b 100644 --- a/src/protocol/leaf_template.rs +++ b/src/protocol/leaf_template.rs @@ -123,6 +123,7 @@ macro_rules! unshell_leaf { == <$Session as $crate::protocol::Session<$State>>::PROCEDURE_ID { $crate::protocol::dispatch_session::<$State, $Session>( + endpoint, leaf_id, &mut self.state, &mut self.$session_field, diff --git a/src/protocol/runtime.rs b/src/protocol/runtime.rs index ecf1347..c16f5d6 100644 --- a/src/protocol/runtime.rs +++ b/src/protocol/runtime.rs @@ -4,7 +4,7 @@ use crate::{ interface::{InterfaceEventKind, InterfaceStore, InterfaceTarget}, protocol::{ 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 /// interface state when a caller supplied one. pub fn dispatch_session( + endpoint: &Endpoint, leaf_id: u32, leaf: &mut L, family: &mut SessionFamily, @@ -131,12 +132,27 @@ pub fn dispatch_session( } 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 mut init = SessionInit::new(hook_id, packet_path); match S::init(leaf, packet, &mut init) { - SessionInitResult::Created(state) => { - family.entries.push(SessionEntry::new(hook_id, state)); + Ok(state) => { + family.entries.push(SessionEntry::new(hook_id, path, state)); if let Some(store) = interface.as_mut() { store.record_for( @@ -150,7 +166,7 @@ pub fn dispatch_session( ); } } - SessionInitResult::Rejected => { + Err(SessionInitError::Rejected) => { if let Some(store) = interface.as_mut() { store.record_for( target, @@ -163,7 +179,15 @@ pub fn dispatch_session( ); } } - 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() { store.record_for( target, @@ -203,14 +227,9 @@ pub fn update_session_family( let started_ns = interface.as_ref().and_then(|store| store.now_ns()); let outbox_start = entry.outbox.len(); - let reply_path = S::reply_path(&entry.state).to_vec(); + let path = entry.path.clone(); let status = { - let mut ctx = SessionCtx::new( - entry.hook_id, - reply_path, - S::PROCEDURE_ID, - &mut entry.outbox, - ); + let mut ctx = SessionCtx::new(entry.hook_id, path, S::PROCEDURE_ID, &mut entry.outbox); S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx) }; diff --git a/src/protocol/session.rs b/src/protocol/session.rs index eefb3bb..0889226 100644 --- a/src/protocol/session.rs +++ b/src/protocol/session.rs @@ -18,16 +18,12 @@ use crate::interface::SessionView; /// const PROCEDURE_ID: u32 = 7; /// type State = MySessionState; /// -/// fn reply_path(state: &Self::State) -> &[u32] { -/// &state.reply_path -/// } -/// /// fn init( /// leaf: &mut MyLeafState, /// packet: Packet, /// ctx: &mut SessionInit, -/// ) -> SessionInitResult { -/// SessionInitResult::Created(MySessionState::from_open(leaf, packet, ctx)) +/// ) -> Result { +/// Ok(MySessionState::from_open(leaf, packet, ctx)) /// } /// /// fn update( @@ -50,20 +46,16 @@ pub trait Session { /// Application state stored for one live hook. 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. /// - /// Returning [`SessionInitResult::RejectedWith`] lets the generated leaf route a - /// protocol-level failure response with the same retry guarantees as normal - /// output. Returning [`SessionInitResult::Rejected`] silently consumes the packet. - fn init(leaf: &mut L, packet: Packet, ctx: &mut SessionInit) -> SessionInitResult; + /// The generated runtime derives all response routing from hook state. Session + /// initialization therefore returns only application state or a protocol-level + /// rejection; it never stores or receives a caller reply path. + fn init( + leaf: &mut L, + packet: Packet, + ctx: &mut SessionInit, + ) -> Result; /// 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. -pub enum SessionInitResult { - /// A new session was created and should be stored by the generated leaf. - Created(S), - - /// The packet was intentionally consumed without creating state or a response. +/// Error returned when a packet cannot create a new session. +pub enum SessionInitError { + /// The packet was intentionally consumed without creating state or sending output. Rejected, - /// The packet was rejected with a response that the generated leaf must route. - RejectedWith(Packet), + /// The packet was rejected with response data that should be sent on the same hook. + Response { + /// Raw `Packet::data` for the response frame. + data: Vec, + + /// 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) -> Self { + Self::Response { + data, + end_hook: false, + } + } + + /// Creates a final response for a rejected session open. + pub fn response_final(data: Vec) -> Self { + Self::Response { + data, + end_hook: true, + } + } } /// 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. pub struct SessionCtx<'a> { hook_id: HookID, - reply_path: Vec, + path: Vec, procedure_id: u32, outbox: &'a mut PacketQueue, } @@ -162,13 +180,13 @@ impl<'a> SessionCtx<'a> { /// Creates a context for one session update call. pub fn new( hook_id: HookID, - reply_path: Vec, + path: Vec, procedure_id: u32, outbox: &'a mut PacketQueue, ) -> Self { Self { hook_id, - reply_path, + path, procedure_id, outbox, } @@ -179,11 +197,6 @@ impl<'a> SessionCtx<'a> { 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. pub fn send(&mut self, opcode: u8, data: &[u8]) { self.send_frame(opcode, data, false); @@ -233,7 +246,7 @@ impl<'a> SessionCtx<'a> { self.outbox.push_back(Packet { hook_id: self.hook_id, end_hook, - path: self.reply_path.clone(), + path: self.path.clone(), procedure_id: self.procedure_id, data, }); @@ -249,6 +262,13 @@ pub struct SessionEntry { /// Hook id associated with this live session. 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, + /// Application-owned session state. pub state: S, @@ -300,9 +320,10 @@ impl Default for SessionFamily { impl SessionEntry { /// 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, state: S) -> Self { Self { hook_id, + path, state, inbox: PacketQueue::new(), outbox: PacketQueue::new(),