feat: complete protocol spec and initial implementation

- Write PROTOCOL.md with full wire format spec and 8 real-world scenario
  analyses (reconnect, multi-operator, large files, AV evasion, router crash,
  malformed packets, future pivoting)

- Rewrite workspace structure:
  - unshell lib: protocol types (PacketHeader, TreeRequest/Response,
    HandshakeMessage/Ack), Transport trait, TcpTransport, Tree routing
  - ush-router: router binary with per-node threads, NodeRegistry with
    longest-prefix path matching, packet relay
  - ush-payload: implant binary with reconnect loop, module tree, InfoModule
  - ush-cli: operator REPL with rustyline, session management, command parser

- Protocol design: two-part rkyv frame [header][payload]; router reads only
  header for routing, payload bytes forwarded opaque

- All code documented with doc comments and examples
- Zero warnings, zero errors across entire workspace
- 32 tests pass (unit tests for tree routing, TCP transport, framing,
  command parsing, node registry)
This commit is contained in:
Michael Mikovsky
2026-04-20 23:38:02 -06:00
parent 959ea469a8
commit fcb3b2be17
30 changed files with 4623 additions and 658 deletions
+283 -67
View File
@@ -1,115 +1,331 @@
// Choose if the macros are enabled based on the feature setting
#[cfg(feature = "log")]
mod log_enabled;
//! # Logger Module
//!
//! A lightweight, no_std-compatible logging system.
//!
//! ## Usage
//!
//! ```rust
//! use unshell::{info, warn, error};
//! use unshell::logger::Logger;
//!
//! // Uses the default (no-op) logger until one is installed.
//! info!("Starting up");
//! warn!("Something is off");
//! error!("Critical failure");
//! ```
//!
//! ## Installing a logger
//!
//! Call [`set_logger`] with any type that implements [`Logger`]:
//!
//! ```rust,no_run
//! use unshell::logger::{Logger, LogLevel, Record, set_logger};
//!
//! struct StdoutLogger;
//! impl Logger for StdoutLogger {
//! fn log(&self, record: &Record<'_>) {
//! // In a no_std environment you would use the `unix-print` crate
//! // or write to a pre-opened file descriptor.
//! let _ = record; // placeholder
//! }
//! }
//!
//! static MY_LOGGER: StdoutLogger = StdoutLogger;
//! set_logger(&MY_LOGGER);
//! ```
//!
//! ## Thread safety
//!
//! The global logger pointer is set **once at startup**, before any threads
//! are spawned. After that, it is only read (never written). This is safe
//! because:
//!
//! 1. The payload is single-threaded.
//! 2. The router and CLI set the logger before spawning node threads.
//!
//! If you need to change the logger after threads start, synchronise access
//! with a `Mutex` or an atomic pointer in your logger implementation.
#[cfg(not(feature = "log"))]
mod log_disabled;
// ---------------------------------------------------------------------------
// Log levels
// ---------------------------------------------------------------------------
mod pretty_logger;
use alloc::boxed::Box;
use alloc::string::String;
pub use pretty_logger::PrettyLogger;
pub use pretty_logger::log;
pub static mut IS_DEFAULT_LOGGER: bool = true;
static mut LOGGER: &dyn Logger = &DefaultLogger;
#[derive(Debug)]
/// The severity level of a log record.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
/// Verbose diagnostic information.
Debug,
/// Normal operational messages.
Info,
/// Something unexpected happened but execution can continue.
Warn,
/// A serious error occurred.
Error,
}
#[derive(Debug)]
pub struct Record {
log_level: LogLevel,
location: Option<String>,
// line: u32,
time: Option<u64>,
message: String,
}
pub trait Logger {
fn log(&self, log: Record);
}
struct DefaultLogger;
impl Logger for DefaultLogger {
fn log(&self, _: Record) {}
}
#[allow(unused_variables)]
pub fn set_logger_box(logger: Box<dyn Logger>) {
#[cfg(feature = "log")]
unsafe {
LOGGER = Box::leak(logger);
IS_DEFAULT_LOGGER = false;
impl LogLevel {
/// Short uppercase label, suitable for log line prefixes.
///
/// # Example
///
/// ```rust
/// use unshell::logger::LogLevel;
/// assert_eq!(LogLevel::Info.as_str(), "INFO");
/// ```
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Debug => "DEBUG",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
}
}
}
// ---------------------------------------------------------------------------
// Log record
// ---------------------------------------------------------------------------
/// A single log entry passed to a [`Logger`].
///
/// Borrows from the call site to avoid heap allocation on the hot path.
pub struct Record<'a> {
/// Severity level.
pub level: LogLevel,
/// The log message.
pub message: &'a str,
/// Source file, if available (e.g. `file!()`).
pub file: Option<&'static str>,
/// Source line number, if available (e.g. `line!()`).
pub line: Option<u32>,
}
// ---------------------------------------------------------------------------
// Logger trait
// ---------------------------------------------------------------------------
/// A sink for log records.
///
/// Implement this to direct log output wherever you want (stdout, a file,
/// a TCP connection, a memory buffer for tests).
pub trait Logger: Sync {
/// Receive and process a log record.
fn log(&self, record: &Record<'_>);
}
// ---------------------------------------------------------------------------
// Global logger state
// ---------------------------------------------------------------------------
/// The no-op logger used before any logger is installed.
struct NullLogger;
impl Logger for NullLogger {
fn log(&self, _record: &Record<'_>) {}
}
/// The global logger pointer.
///
/// Written once at startup via [`set_logger`], then only read.
/// # Safety
/// This is `static mut` to avoid a dependency on synchronisation primitives
/// in a no_std context. It is safe as long as `set_logger` is called before
/// any threads are spawned (see module-level docs).
static mut GLOBAL_LOGGER: &dyn Logger = &NullLogger;
/// Install a new global logger.
///
/// Must be called **before** spawning any threads. After this call, all
/// `info!`, `warn!`, `error!`, and `debug!` macros route to this logger.
///
/// # Safety
///
/// This function writes to a `static mut`. It is safe when called exactly
/// once at program startup before any other threads exist.
///
/// # Example
///
/// ```rust,no_run
/// use unshell::logger::{Logger, Record, set_logger};
///
/// static MY_LOGGER: MyLogger = MyLogger;
/// set_logger(&MY_LOGGER);
///
/// # struct MyLogger;
/// # impl Logger for MyLogger { fn log(&self, _: &Record<'_>) {} }
/// ```
pub fn set_logger(logger: &'static dyn Logger) {
// SAFETY: called once at startup before any threads are spawned.
#[allow(static_mut_refs)]
unsafe {
LOGGER = logger;
IS_DEFAULT_LOGGER = false;
GLOBAL_LOGGER = logger;
}
}
pub fn add_record(
log_level: LogLevel,
location: Option<String>,
time: Option<u64>,
message: String,
) {
logger().log(Record {
log_level,
location,
time,
/// Return a reference to the currently installed logger.
///
/// Used internally by the logging macros.
#[must_use]
pub fn global_logger() -> &'static dyn Logger {
// SAFETY: GLOBAL_LOGGER is only written once (at startup) and is
// read-only thereafter. No data race is possible.
#[allow(static_mut_refs)]
unsafe {
GLOBAL_LOGGER
}
}
/// Log a record through the global logger.
///
/// This is the low-level function called by the macros. Prefer using the
/// `info!`, `warn!`, `error!`, and `debug!` macros directly.
pub fn log(level: LogLevel, message: &str, file: Option<&'static str>, line: Option<u32>) {
global_logger().log(&Record {
level,
message,
file,
line,
});
}
pub fn logger() -> &'static dyn Logger {
unsafe { LOGGER }
// ---------------------------------------------------------------------------
// A minimal stdout logger for use in std binaries (router, CLI)
// ---------------------------------------------------------------------------
/// A simple logger that prints to stderr.
///
/// Suitable for the router and operator CLI binaries.
/// Do not use in the payload binary (which may not have stderr available).
///
/// # Example
///
/// ```rust,no_run
/// use unshell::logger::{StderrLogger, set_logger};
///
/// static LOGGER: StderrLogger = StderrLogger::new(unshell::logger::LogLevel::Info);
/// set_logger(&LOGGER);
/// ```
pub struct StderrLogger {
/// Minimum level to log. Records below this level are discarded.
min_level: LogLevel,
}
#[allow(dead_code, improper_ctypes_definitions)]
pub type SetupLogger = extern "C" fn(logger: &'static dyn Logger);
#[unsafe(no_mangle)]
#[allow(improper_ctypes_definitions)]
pub extern "C" fn setup_logger(logger: &'static dyn Logger) {
set_logger(logger);
impl StderrLogger {
/// Create a new `StderrLogger` that logs records at `min_level` and above.
///
/// # Example
///
/// ```rust
/// use unshell::logger::{StderrLogger, LogLevel};
/// let logger = StderrLogger::new(LogLevel::Info);
/// ```
#[must_use]
pub const fn new(min_level: LogLevel) -> Self {
Self { min_level }
}
}
// Macro Definitions
impl Logger for StderrLogger {
fn log(&self, record: &Record<'_>) {
if record.level < self.min_level {
return;
}
// eprintln! and String require std (available only with the `tcp` feature).
// In no_std builds this method is a no-op. The payload uses a different
// logger (or the null logger) in no_std contexts.
#[cfg(feature = "tcp")]
{
use alloc::string::String;
let location = match (record.file, record.line) {
(Some(f), Some(l)) => {
let mut s = String::from(f);
s.push(':');
s.push_str(&format!("{l}"));
s
}
_ => String::new(),
};
if location.is_empty() {
eprintln!("[{}] {}", record.level.as_str(), record.message);
} else {
eprintln!("[{}] {} - {}", record.level.as_str(), record.message, location);
}
}
}
}
// ---------------------------------------------------------------------------
// Logging macros
// ---------------------------------------------------------------------------
/// Log at [`LogLevel::Debug`] level.
///
/// ```rust
/// use unshell::debug;
/// debug!("loop iteration {}", 42);
/// ```
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {
$crate::log!($crate::logger::LogLevel::Debug, $($arg)*)
$crate::logger::log(
$crate::logger::LogLevel::Debug,
&format!($($arg)*),
Some(file!()),
Some(line!()),
)
};
}
/// Log at [`LogLevel::Info`] level.
///
/// ```rust
/// use unshell::info;
/// info!("server started on port {}", 9000);
/// ```
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {
$crate::log!($crate::logger::LogLevel::Info, $($arg)*)
$crate::logger::log(
$crate::logger::LogLevel::Info,
&format!($($arg)*),
Some(file!()),
Some(line!()),
)
};
}
/// Log at [`LogLevel::Warn`] level.
///
/// ```rust
/// use unshell::warn;
/// warn!("unexpected path: {}", "/unknown");
/// ```
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
$crate::log!($crate::logger::LogLevel::Warn, $($arg)*)
$crate::logger::log(
$crate::logger::LogLevel::Warn,
&format!($($arg)*),
Some(file!()),
Some(line!()),
)
};
}
/// Log at [`LogLevel::Error`] level.
///
/// ```rust
/// use unshell::error;
/// error!("connection failed: {}", "timeout");
/// ```
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
$crate::log!($crate::logger::LogLevel::Error, $($arg)*)
$crate::logger::log(
$crate::logger::LogLevel::Error,
&format!($($arg)*),
Some(file!()),
Some(line!()),
)
};
}