mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
393 lines
8.5 KiB
Markdown
393 lines
8.5 KiB
Markdown
|
|
# Tree Protocol Specification
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The Tree Protocol is a lightweight, extensible message format designed for hierarchical message routing in a modular C2 (Command & Control) system. It provides RPC-like interactions, streaming support, event notifications, and peer-to-peer pivoting capabilities.
|
||
|
|
|
||
|
|
## Design Principles
|
||
|
|
|
||
|
|
1. **Loose typing** - Actions and payloads are flexible JSON values, not strict enums
|
||
|
|
2. **Namespacing** - Use dot notation for action categorization (e.g., `rpc.call`, `stream.data`)
|
||
|
|
3. **Extensibility** - Add new capabilities without modifying core structure
|
||
|
|
4. **Simplicity** - Minimal required fields, optional metadata for flexibility
|
||
|
|
|
||
|
|
## Message Structure
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "uuid-string",
|
||
|
|
"source": "path/to/sender",
|
||
|
|
"target": "path/to/recipient",
|
||
|
|
"action": "action.name",
|
||
|
|
"payload": {},
|
||
|
|
"routing": {},
|
||
|
|
"meta": {}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Field Definitions
|
||
|
|
|
||
|
|
| Field | Type | Required | Description |
|
||
|
|
|-------|------|----------|-------------|
|
||
|
|
| `id` | string | No | Unique message identifier for correlation |
|
||
|
|
| `source` | string/array | No | Origin path for responses |
|
||
|
|
| `target` | string/array | No | Destination path for routing |
|
||
|
|
| `action` | value | Yes | Operation to perform |
|
||
|
|
| `payload` | any | No | Data for the action |
|
||
|
|
| `routing` | object | No | P2P/pivoting metadata |
|
||
|
|
| `meta` | object | No | Extensible metadata |
|
||
|
|
|
||
|
|
## Action System
|
||
|
|
|
||
|
|
Actions are loose values - any JSON that components can interpret:
|
||
|
|
|
||
|
|
```json
|
||
|
|
"action": "query"
|
||
|
|
"action": "rpc.call"
|
||
|
|
"action": "stream.open"
|
||
|
|
"action": {"name": "custom", "version": 1}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Common Action Categories
|
||
|
|
|
||
|
|
| Category | Prefix | Purpose |
|
||
|
|
|----------|--------|---------|
|
||
|
|
| Core | `query`, `create`, `delete`, `update` | CRUD operations |
|
||
|
|
| RPC | `rpc.call`, `rpc.response` | Remote procedure calls |
|
||
|
|
| Streams | `stream.open`, `stream.data`, `stream.close` | Bidirectional streams |
|
||
|
|
| Events | `subscribe`, `event` | Event notifications |
|
||
|
|
| Network | `connect`, `disconnect`, `forward` | Connection handling |
|
||
|
|
|
||
|
|
## Payload Formats
|
||
|
|
|
||
|
|
Payloads vary by action - components interpret them flexibly:
|
||
|
|
|
||
|
|
### RPC Payload
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "rpc.call",
|
||
|
|
"target": ["endpoints", "ep1", "components", "tcp-client"],
|
||
|
|
"payload": {
|
||
|
|
"method": "connect",
|
||
|
|
"params": {"address": "127.0.0.1", "port": 443}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "rpc.response",
|
||
|
|
"payload": {
|
||
|
|
"success": true,
|
||
|
|
"result": {"connected": true}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Stream Payload
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "stream.open",
|
||
|
|
"payload": {
|
||
|
|
"channel": "stdio",
|
||
|
|
"mode": "bidirectional"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "stream.data",
|
||
|
|
"payload": {
|
||
|
|
"channel": "stdout",
|
||
|
|
"data": "SGVsbG8gd29ybGQ=", // base64 encoded
|
||
|
|
"chunk": 0,
|
||
|
|
"total": 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "stream.close",
|
||
|
|
"payload": {
|
||
|
|
"channel": "stdio",
|
||
|
|
"reason": "eof"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Event Payload
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "subscribe",
|
||
|
|
"target": ["endpoints", "ep1", "logs"],
|
||
|
|
"payload": {
|
||
|
|
"event": "new_entry",
|
||
|
|
"callback": "components/event-handler"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "event",
|
||
|
|
"source": ["endpoints", "ep1", "logs"],
|
||
|
|
"payload": {
|
||
|
|
"event": "new_entry",
|
||
|
|
"data": "some log message"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### P2P Routing Payload
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"action": "forward",
|
||
|
|
"target": ["peers", "peer-2", "endpoints", "ep2"],
|
||
|
|
"routing": {
|
||
|
|
"via": "peer-1",
|
||
|
|
"hop": 1,
|
||
|
|
"max_hops": 3
|
||
|
|
},
|
||
|
|
"payload": {...}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Response Pattern
|
||
|
|
|
||
|
|
All responses use a generic format:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "req-123",
|
||
|
|
"action": "response",
|
||
|
|
"source": ["endpoints", "ep1"],
|
||
|
|
"payload": {
|
||
|
|
"success": true,
|
||
|
|
"result": {"connected": true},
|
||
|
|
"error": null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "req-123",
|
||
|
|
"action": "response",
|
||
|
|
"payload": {
|
||
|
|
"success": false,
|
||
|
|
"error": {
|
||
|
|
"code": 404,
|
||
|
|
"message": "not found"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Path Format
|
||
|
|
|
||
|
|
Paths can be represented in multiple ways:
|
||
|
|
|
||
|
|
```json
|
||
|
|
"target": "components/tcp-client"
|
||
|
|
"target": ["components", "tcp-client"]
|
||
|
|
"target": ["endpoints", "ep1", "connections", "conn-1"]
|
||
|
|
```
|
||
|
|
|
||
|
|
## Message Types (Optional Wrapper)
|
||
|
|
|
||
|
|
For transport-level distinction:
|
||
|
|
|
||
|
|
| Type | Description |
|
||
|
|
|------|-------------|
|
||
|
|
| `req` | Request - expecting a response |
|
||
|
|
| `resp` | Response - reply to a request |
|
||
|
|
| `event` | Unsolicited notification |
|
||
|
|
| `stream` | Stream data message |
|
||
|
|
|
||
|
|
Example with type wrapper:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "msg-123",
|
||
|
|
"type": "req",
|
||
|
|
"target": ["components", "tcp-client"],
|
||
|
|
"action": "rpc.call",
|
||
|
|
"payload": {"method": "connect", "params": {...}}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Binary Framing (Optional)
|
||
|
|
|
||
|
|
For bandwidth-constrained links:
|
||
|
|
|
||
|
|
```
|
||
|
|
[4 bytes: length][1 byte: flags][id (16 bytes, optional)][payload...]
|
||
|
|
```
|
||
|
|
|
||
|
|
- Length: Big-endian u32
|
||
|
|
- Flags: 0x01 = compressed, 0x02 = encrypted
|
||
|
|
- ID: UUID v4 (optional, for correlation)
|
||
|
|
|
||
|
|
## Examples
|
||
|
|
|
||
|
|
### Full RPC Call
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "req-123",
|
||
|
|
"source": "server",
|
||
|
|
"target": ["endpoints", "ep1", "components", "tcp-client"],
|
||
|
|
"action": "rpc.call",
|
||
|
|
"payload": {
|
||
|
|
"method": "connect",
|
||
|
|
"params": {"address": "127.0.0.1", "port": 8080}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Response
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "resp-123",
|
||
|
|
"source": ["endpoints", "ep1"],
|
||
|
|
"target": "server",
|
||
|
|
"action": "response",
|
||
|
|
"payload": {
|
||
|
|
"success": true,
|
||
|
|
"result": {
|
||
|
|
"connected": true,
|
||
|
|
"local_addr": "192.168.1.100:12345",
|
||
|
|
"remote_addr": "127.0.0.1:8080"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Log Subscription + Events
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "sub-1",
|
||
|
|
"source": "server",
|
||
|
|
"target": ["endpoints", "ep1", "logs"],
|
||
|
|
"action": "subscribe",
|
||
|
|
"payload": {
|
||
|
|
"event": "new_entry",
|
||
|
|
"callback": ["components", "log-forwarder"]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "evt-1",
|
||
|
|
"source": ["endpoints", "ep1", "logs"],
|
||
|
|
"target": ["components", "log-forwarder"],
|
||
|
|
"action": "event",
|
||
|
|
"payload": {
|
||
|
|
"event": "new_entry",
|
||
|
|
"data": "2024-01-15 10:30:45 - Connection established"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### P2P Forward
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "req-456",
|
||
|
|
"source": "server",
|
||
|
|
"target": ["endpoints", "ep2", "components", "shell"],
|
||
|
|
"action": "rpc.call",
|
||
|
|
"routing": {
|
||
|
|
"via": "peer-1",
|
||
|
|
"hop": 1,
|
||
|
|
"max_hops": 3
|
||
|
|
},
|
||
|
|
"payload": {
|
||
|
|
"method": "execute",
|
||
|
|
"params": {"command": "whoami"}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Implementation Notes
|
||
|
|
|
||
|
|
### Action Matching
|
||
|
|
|
||
|
|
```rust
|
||
|
|
impl TreeMessage {
|
||
|
|
/// Check if action matches a pattern
|
||
|
|
pub fn action_is(&self, action: &str) -> bool {
|
||
|
|
match &self.action {
|
||
|
|
Value::String(s) => s == action || s.ends_with(&format!(".{}", action)),
|
||
|
|
_ => false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get method name from RPC payload
|
||
|
|
pub fn get_method(&self) -> Option<String> {
|
||
|
|
self.payload.get("method")
|
||
|
|
.and_then(|m| m.as_str())
|
||
|
|
.map(String::from)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get stream channel from payload
|
||
|
|
pub fn get_channel(&self) -> Option<String> {
|
||
|
|
self.payload.get("channel")
|
||
|
|
.and_then(|c| c.as_str())
|
||
|
|
.map(String::from)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Component Interpretation
|
||
|
|
|
||
|
|
Components define their own action handlers:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
impl TreeElement for TcpClient {
|
||
|
|
fn send_message(&mut self, target: Value, message: Value) -> Value {
|
||
|
|
match message.get("method").and_then(|m| m.as_str()) {
|
||
|
|
Some("connect") => self.handle_connect(message),
|
||
|
|
Some("send") => self.handle_send(message),
|
||
|
|
Some("recv") => self.handle_recv(message),
|
||
|
|
_ => json!({"error": "unknown method"}),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Comparison to Other Protocols
|
||
|
|
|
||
|
|
| Aspect | Tree Protocol | OpenC2 | OST-C2 | Mythic |
|
||
|
|
|--------|--------------|--------|--------|--------|
|
||
|
|
| Typing | Loose/JSON | Strict enum | Binary enum | JSON |
|
||
|
|
| Actions | Namespaced strings | Fixed enum | Type/Code | Action strings |
|
||
|
|
| RPC | Yes | No | Limited | Yes |
|
||
|
|
| Streams | Yes | No | No | No |
|
||
|
|
| Events | Yes | No | No | Yes |
|
||
|
|
| P2P | Via paths | No | Yes | Via delegates |
|
||
|
|
| Extensibility | High | Medium | Low | Medium |
|
||
|
|
|
||
|
|
## Extensibility Points
|
||
|
|
|
||
|
|
1. **New actions**: Add any namespaced string - no code changes needed
|
||
|
|
2. **Payload structure**: Any JSON - components interpret as needed
|
||
|
|
3. **Meta field**: Add transport, timing, or custom metadata
|
||
|
|
4. **Routing field**: Add P2P-specific metadata as needed
|
||
|
|
5. **Namespacing**: Use prefixes like `openc2.`, `custom.`, `vendor.` for compatibility
|
||
|
|
|
||
|
|
## Serialization
|
||
|
|
|
||
|
|
- Default: JSON (human-readable, debuggable)
|
||
|
|
- Optional: CBOR (binary, compact)
|
||
|
|
- Custom: Any format via `meta.serialization` hint
|