mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
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:
@@ -1,6 +0,0 @@
|
||||
// Macros that are used that just drop the inside variables
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($level:expr, $fmt:tt) => {{}};
|
||||
($level:expr, $fmt:tt, $($arg:expr),*) => {{}};
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($level:expr, $fmt:tt) => {{
|
||||
use $crate::obfuscate;
|
||||
let log_result = obfuscate::sym_format!($fmt);
|
||||
|
||||
$crate::logger::add_record(
|
||||
$level,
|
||||
|
||||
#[cfg(feature = "log_debug")]
|
||||
Some(String::from(obfuscate::file_symbol!())),
|
||||
#[cfg(not(feature = "log_debug"))]
|
||||
None,
|
||||
|
||||
#[cfg(feature = "log_debug")]
|
||||
Some(std::time::SystemTime::now()),
|
||||
#[cfg(not(feature = "log_debug"))]
|
||||
None,
|
||||
|
||||
|
||||
log_result
|
||||
);
|
||||
}};
|
||||
($level:expr, $fmt:tt, $($arg:expr),*) => {{
|
||||
use $crate::obfuscate;
|
||||
let log_result = obfuscate::sym_format!($fmt, $($arg),*);
|
||||
|
||||
$crate::logger::add_record(
|
||||
$level,
|
||||
|
||||
#[cfg(feature = "log_debug")]
|
||||
Some(String::from(obfuscate::file_symbol!())),
|
||||
#[cfg(not(feature = "log_debug"))]
|
||||
None,
|
||||
|
||||
#[cfg(feature = "log_debug")]
|
||||
Some(std::time::SystemTime::now()),
|
||||
#[cfg(not(feature = "log_debug"))]
|
||||
None,
|
||||
|
||||
log_result
|
||||
);
|
||||
}};
|
||||
}
|
||||
+283
-67
@@ -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!()),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
use alloc::{boxed::Box, format};
|
||||
|
||||
use crate::logger::{LogLevel, Logger, Record};
|
||||
|
||||
pub struct PrettyLogger {
|
||||
output: Option<Box<dyn Fn(&Record)>>,
|
||||
}
|
||||
|
||||
impl Logger for PrettyLogger {
|
||||
fn log(&self, message: Record) {
|
||||
if let Some(ref func) = self.output {
|
||||
(*func)(&message)
|
||||
}
|
||||
|
||||
log(&message);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(message: &Record) {
|
||||
static DEBUG_COLOR: &str = "\x1b[36m";
|
||||
static INFO_COLOR: &str = "\x1b[32m";
|
||||
static WARN_COLOR: &str = "\x1b[33m";
|
||||
static ERROR_COLOR: &str = "\x1b[31m";
|
||||
|
||||
let log_level = match message.log_level {
|
||||
LogLevel::Debug => format!("{DEBUG_COLOR}DBUG"),
|
||||
LogLevel::Info => format!("{INFO_COLOR}INFO"),
|
||||
LogLevel::Warn => format!("{WARN_COLOR}WARN"),
|
||||
LogLevel::Error => format!("{ERROR_COLOR}ERR!"),
|
||||
};
|
||||
|
||||
match (message.time, &message.location) {
|
||||
(None, None) => {
|
||||
static WHITE: &str = "\x1b[97m";
|
||||
|
||||
unix_print::unix_println!("{} {WHITE}{}", log_level, message.message);
|
||||
}
|
||||
|
||||
#[cfg(feature = "log_debug")]
|
||||
(Some(time), Some(location)) => {
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
let date: DateTime<Utc> = time.into();
|
||||
|
||||
static WHITE: &str = "\x1b[97m";
|
||||
static OFF_WHITE: &str = "\x1b[37m";
|
||||
static TIME_COLOR: &str = "\x1b[36m";
|
||||
static GREY: &str = "\x1b[90m";
|
||||
|
||||
unix_print::unix_println!(
|
||||
"{OFF_WHITE}[{TIME_COLOR}{}{OFF_WHITE}] {} {WHITE}{} {GREY}{}{WHITE}",
|
||||
date,
|
||||
log_level,
|
||||
message.message,
|
||||
location
|
||||
);
|
||||
}
|
||||
|
||||
_ => unreachable!("Invalid log configuration"),
|
||||
}
|
||||
}
|
||||
|
||||
impl PrettyLogger {
|
||||
pub fn init() {
|
||||
if unsafe { crate::logger::IS_DEFAULT_LOGGER } {
|
||||
crate::logger::set_logger_box(Box::new(PrettyLogger { output: None }));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_output<T>(output: T)
|
||||
where
|
||||
T: Fn(&Record) + 'static,
|
||||
{
|
||||
if !unsafe { crate::logger::IS_DEFAULT_LOGGER } {
|
||||
crate::logger::set_logger_box(Box::new(PrettyLogger {
|
||||
output: Some(Box::new(output)),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user