mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Make new error structs, improve tests, remake file structure.
This commit is contained in:
@@ -1,105 +1,127 @@
|
||||
use crate::{
|
||||
endpoint::{Endpoint, error::EndpointError},
|
||||
packet::Packet,
|
||||
};
|
||||
use crate::{Endpoint, EndpointError, Packet, RouteDirection};
|
||||
|
||||
impl Endpoint {
|
||||
/// Register an inbound packet and route it
|
||||
/// Register an inbound packet and route it through the local endpoint state.
|
||||
///
|
||||
/// Inbound transport data still uses the same local routing rules as packets
|
||||
/// generated by leaves: local destinations are delivered to `inbound`, and
|
||||
/// transit destinations are queued by their immediate next hop.
|
||||
pub fn add_inbound(&mut self, packet: Packet) -> Result<(), EndpointError> {
|
||||
// In case some leaf hasn't assigned the endpoint a path yet.
|
||||
if self.path.is_empty() {
|
||||
return Err(EndpointError::NoAbsoultePathYet);
|
||||
}
|
||||
|
||||
// If the packet is routed towards this endpoint
|
||||
if packet.path == *self.path {
|
||||
// Get the last segment of the path
|
||||
let local_id = self
|
||||
.path
|
||||
.last()
|
||||
.cloned()
|
||||
.ok_or(EndpointError::IncorrectAbsolutePath)?;
|
||||
|
||||
self.inbound.entry(local_id).or_default().push_back(packet);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
let (next_hop, is_upward) = self.next_hop_for(&packet)?;
|
||||
|
||||
if !self.connections.contains(&(next_hop, is_upward)) {
|
||||
return Err(EndpointError::RouteNotExist);
|
||||
}
|
||||
|
||||
self.queue_outbound(packet, next_hop)
|
||||
}
|
||||
self.route_packet(packet)
|
||||
}
|
||||
|
||||
/// Register an outbound packet produced locally and route it to the next queue.
|
||||
///
|
||||
/// This intentionally shares the same implementation as [`Self::add_inbound`]
|
||||
/// so local leaf output and received transport packets cannot drift into subtly
|
||||
/// different route semantics.
|
||||
pub fn add_outbound(&mut self, packet: Packet) -> Result<(), EndpointError> {
|
||||
// In case some leaf hasn't assigned the endpoint a path yet.
|
||||
if self.path.is_empty() {
|
||||
return Err(EndpointError::NoAbsoultePathYet);
|
||||
}
|
||||
self.route_packet(packet)
|
||||
}
|
||||
|
||||
// If this packet is routed towards this node
|
||||
if packet.path == *self.path {
|
||||
// Grab the last endpoint ID
|
||||
/// Route a packet by classifying its destination and mutating exactly one queue.
|
||||
///
|
||||
/// Hook cleanup is deliberately last. A packet with `end_hook = true` should not
|
||||
/// tear down local hook state unless the packet has a valid route and is actually
|
||||
/// queued for forwarding. The route branches are kept inline rather than using
|
||||
/// an intermediate decision enum so size-focused builds have less structure to
|
||||
/// optimize away.
|
||||
fn route_packet(&mut self, packet: Packet) -> Result<(), EndpointError> {
|
||||
self.ensure_path_is_set()?;
|
||||
|
||||
if packet.path == self.path {
|
||||
let local_id = self
|
||||
.path
|
||||
.last()
|
||||
.cloned()
|
||||
.ok_or(EndpointError::IncorrectAbsolutePath)?;
|
||||
.copied()
|
||||
.ok_or(EndpointError::EndpointPathUnset)?;
|
||||
|
||||
// Add it to the inbound queue
|
||||
self.inbound.entry(local_id).or_default().push_back(packet);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (next_hop, is_upward) = self.next_hop_for(&packet)?;
|
||||
|
||||
if !self.connections.contains(&(next_hop, is_upward)) {
|
||||
return Err(EndpointError::RouteNotExist);
|
||||
}
|
||||
|
||||
self.queue_outbound(packet, next_hop)
|
||||
}
|
||||
|
||||
fn queue_outbound(&mut self, packet: Packet, next_hop: u32) -> Result<(), EndpointError> {
|
||||
if packet.end_hook {
|
||||
self.hooks.remove(&packet.hook_id);
|
||||
}
|
||||
|
||||
self.outbound.entry(next_hop).or_default().push_back(packet);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_hop_for(&self, packet: &Packet) -> Result<(u32, bool), EndpointError> {
|
||||
// Direction is derived from the local path. The packet never gets to declare
|
||||
// whether it is moving upward, because that would make the trust boundary spoofable.
|
||||
if packet.path.starts_with(&self.path) {
|
||||
let next_hop = packet
|
||||
.path
|
||||
.get(self.path.len())
|
||||
.cloned()
|
||||
.ok_or(EndpointError::IncorrectAbsolutePath)?;
|
||||
.copied()
|
||||
.ok_or(EndpointError::DestinationOutsideLocalTree)?;
|
||||
|
||||
Ok((next_hop, false))
|
||||
} else if self.path.starts_with(&packet.path) {
|
||||
// SECURITY: All upward-routed packets must be checked against local hook state.
|
||||
self.ensure_registered_connection(next_hop, RouteDirection::Downward)?;
|
||||
self.queue_outbound(packet, next_hop, RouteDirection::Downward);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.path.starts_with(&packet.path) {
|
||||
// Upward-routed packets must be tied to local hook state. Otherwise a
|
||||
// peer could forge a packet to an ancestor by choosing an older path.
|
||||
if !self.hooks.contains_key(&packet.hook_id) {
|
||||
return Err(EndpointError::HookNotExist);
|
||||
return Err(EndpointError::UnknownHook {
|
||||
hook_id: packet.hook_id,
|
||||
});
|
||||
}
|
||||
|
||||
let parent_index = self
|
||||
.path
|
||||
.len()
|
||||
.checked_sub(2)
|
||||
.ok_or(EndpointError::RouteNotExist)?;
|
||||
.ok_or(EndpointError::MissingParentRoute)?;
|
||||
|
||||
Ok((self.path[parent_index], true))
|
||||
let next_hop = self.path[parent_index];
|
||||
self.ensure_registered_connection(next_hop, RouteDirection::Upward)?;
|
||||
self.queue_outbound(packet, next_hop, RouteDirection::Upward);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(EndpointError::DestinationOutsideLocalTree)
|
||||
}
|
||||
|
||||
/// Reject routing before path-relative decisions when no absolute path is known.
|
||||
///
|
||||
/// This preserves the current runtime sentinel where an empty path means the
|
||||
/// endpoint has not been attached to the tree yet.
|
||||
fn ensure_path_is_set(&self) -> Result<(), EndpointError> {
|
||||
if self.path.is_empty() {
|
||||
Err(EndpointError::EndpointPathUnset)
|
||||
} else {
|
||||
Err(EndpointError::IncorrectAbsolutePath)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that the derived adjacent endpoint is registered in this direction.
|
||||
///
|
||||
/// The current connection table stores direction as a boolean. Keeping the bool
|
||||
/// conversion here confines that legacy representation to one place in routing.
|
||||
fn ensure_registered_connection(
|
||||
&self,
|
||||
next_hop: u32,
|
||||
direction: RouteDirection,
|
||||
) -> Result<(), EndpointError> {
|
||||
let is_upward = matches!(direction, RouteDirection::Upward);
|
||||
|
||||
if self.connections.contains(&(next_hop, is_upward)) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(EndpointError::MissingConnection {
|
||||
next_hop,
|
||||
direction,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue `packet` after all route validation has already succeeded.
|
||||
///
|
||||
/// `end_hook` closes local hook state only when hook traffic is moving upward
|
||||
/// toward the hook host. Downward calls may carry a response hook id, but that
|
||||
/// id is only a promise for future upward traffic and must not delete local
|
||||
/// state if it happens to collide with an existing hook id.
|
||||
fn queue_outbound(&mut self, packet: Packet, next_hop: u32, direction: RouteDirection) {
|
||||
if matches!(direction, RouteDirection::Upward) && packet.end_hook {
|
||||
self.hooks.remove(&packet.hook_id);
|
||||
}
|
||||
|
||||
self.outbound.entry(next_hop).or_default().push_back(packet);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user