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
+88
View File
@@ -0,0 +1,88 @@
//! # Info Module
//!
//! Provides basic system information about the target at `/info`.
//!
//! ## Supported requests
//!
//! | Path | RequestType | Returns |
//! |---|---|---|
//! | `/info` | `Read` | UTF-8 string: OS name, arch, hostname |
//! | `/info` | `GetProcedures` | List of available procedures |
//!
//! ## Example
//!
//! From the operator CLI:
//! ```text
//! unshell [agents/abc123]> read info
//! linux x86_64 hostname=target-machine
//! ```
use unshell::protocol::{
content, ProcedureDescriptor, RequestType, ResponseStatus, TreeRequest, TreeResponse,
};
use unshell::tree::Endpoint;
/// Returns basic system information about the target host.
pub struct InfoModule;
impl Endpoint for InfoModule {
fn handle(&mut self, request: TreeRequest) -> TreeResponse {
match request.request_type {
RequestType::Read => handle_read(request),
RequestType::GetProcedures => handle_get_procedures(request),
_ => TreeResponse {
request_id: request.request_id,
status: ResponseStatus::UnsupportedOperation,
content_type: content::NONE.to_owned(),
data: Vec::new(),
},
}
}
}
/// Return a one-line system summary.
fn handle_read(request: TreeRequest) -> TreeResponse {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let hostname = hostname();
let info = format!("os={os} arch={arch} hostname={hostname}");
TreeResponse {
request_id: request.request_id,
status: ResponseStatus::Ok,
content_type: content::UTF8_STRING.to_owned(),
data: info.into_bytes(),
}
}
/// Return a list of procedures this module supports.
fn handle_get_procedures(request: TreeRequest) -> TreeResponse {
let procedures = vec![ProcedureDescriptor {
name: "read".to_owned(),
description: "Returns os, arch, and hostname of this target".to_owned(),
}];
let Ok(payload) = rkyv::to_bytes::<rkyv::rancor::Error>(&procedures) else {
return TreeResponse {
request_id: request.request_id,
status: ResponseStatus::ExecutionError,
content_type: content::NONE.to_owned(),
data: Vec::new(),
};
};
TreeResponse {
request_id: request.request_id,
status: ResponseStatus::Ok,
content_type: content::PROCEDURE_LIST.to_owned(),
data: payload.to_vec(),
}
}
/// Get the system hostname, or "unknown" if unavailable.
fn hostname() -> String {
// std::net::IpAddr doesn't give us hostname; use /etc/hostname or gethostname
// For now, use a simple approach that doesn't require extra deps.
std::fs::read_to_string("/etc/hostname")
.map(|s| s.trim().to_owned())
.unwrap_or_else(|_| "unknown".to_owned())
}
+19
View File
@@ -0,0 +1,19 @@
//! # Payload Modules
//!
//! Each file in this directory implements one payload capability.
//!
//! ## Adding a new module
//!
//! 1. Create a new file `modules/mymodule.rs`.
//! 2. Define a struct implementing [`unshell::tree::Endpoint`].
//! 3. Add `pub mod mymodule;` here.
//! 4. Register it in `main.rs`'s `build_tree()` function:
//! `tree.register("/mymodule", modules::mymodule::MyModule);`
//!
//! ## Module path convention
//!
//! Modules are registered at relative paths (e.g., `/info`, `/shell`).
//! The full path on the network is `{base_path}/{relative_path}`, e.g.,
//! `/agents/abc123/info`.
pub mod info;