RNS Terminal/Shell Integration Research
Summary: Evaluating rnsh, rBrowser, rrcd, and lxmf_terminal for Styrene terminal sessions. Recommended Option B (LXMF control plane + RNS Link data plane) — implemented as TerminalService in styrened.
Date: 2026-01-31 Context: Evaluating existing RNS ecosystem projects for Styrene terminal session design
Executive Summary
Three projects in the Reticulum ecosystem address remote access patterns relevant to Styrene’s terminal session requirements. The most mature is rnsh, which provides SSH-like functionality over RNS using raw Links and Channels (not LXMF). This research recommends learning from rnsh’s architecture while maintaining Styrene’s LXMF-based control plane for consistency with existing fleet management patterns.
Project Analysis
1. rnsh (Remote Shell)
Repository: github.com/acehoss/rnsh Maturity: Production-ready (packaged in OpenBSD ports) Stars: ~78
Architecture: Pure RNS Links (NOT LXMF)
rnsh builds directly on Reticulum’s Packet API, not LXMF. Key architectural decision:
┌─────────────────────────────────────────────────────────────────┐
│ rnsh Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Initiator Listener │
│ (Client) (Server) │
│ ┌──────────┐ ┌──────────┐ │
│ │ rnsh │◄────── RNS Link ─────────►│ rnsh -l │ │
│ │ client │ │ daemon │ │
│ └──────────┘ └────┬─────┘ │
│ │ │
│ ┌────┴─────┐ │
│ │ Child │ │
│ │ Process │ │
│ │ (shell) │ │
│ └──────────┘ │
│ │
│ Transport: RNS Link + Channel (Message API) │
│ NOT LXMF (no store-and-forward, no propagation nodes) │
│ │
└─────────────────────────────────────────────────────────────────┘
Why NOT LXMF: Interactive terminal sessions require real-time bidirectional streaming with minimal latency. LXMF’s store-and-forward model adds latency and doesn’t guarantee message ordering for rapid keystroke sequences.
Protocol Design
Message Types (defined in protocol.py):
| Type | ID | Purpose |
|---|---|---|
NoopMessage | 0 | Keep-alive |
WindowSizeMessage | 2 | Terminal dimensions (rows, cols, pixels) |
ExecuteCommandMessage | 3 | Launch command with args, TTY config |
StreamDataMessage | 4 | stdin (0), stdout (1), stderr (2) data |
VersionInfoMessage | 5 | Protocol handshake |
ErrorMessage | 6 | Error with fatal flag |
CommandExitedMessage | 7 | Process exit code |
Wire Format: Messages use umsgpack serialization, registered with RNS Channel infrastructure.
Session State Machine
┌─────────────────────────────────────────────────────────────────┐
│ Listener State Machine │
├─────────────────────────────────────────────────────────────────┤
│ │
│ LSSTATE_WAIT_IDENT ──► LSSTATE_WAIT_VERS ──► LSSTATE_WAIT_CMD │
│ │ │ │ │
│ │ (auth disabled) │ │ │
│ └───────────────────────┘ │ │
│ ▼ │
│ LSSTATE_RUNNING │
│ │ │
│ ▼ │
│ LSSTATE_TEARDOWN │
│ │
└─────────────────────────────────────────────────────────────────┘
State Timeout: Uses RTT-based adaptive timeouts:
timeout = max(outlet.rtt * timeout_factor, max(outlet.rtt * 2, 10))
Authentication Model
Three modes:
- Identity-based: Validates against allowlist of destination hashes (
-a HASH) - No-auth: Open access with
-nflag - File-based: Reads allowed identities from file, hot-reloadable
Remote identity passed to child process via RNS_REMOTE_IDENTITY environment variable.
Command Execution Policies
| Flag | Behavior |
|---|---|
| (none) | Initiator command preferred, fallback to listener config, then default shell |
-A | Append initiator command to listener command |
-C | Reject any initiator-supplied command |
Latency Characteristics
Current: Stop-and-wait acknowledgment (one packet at a time). Conservative but reliable.
Limitation: “Not terribly fast” for file operations. Sliding-window acknowledgment planned but not implemented.
LoRa Note: Some intermittent failures reported over LoRa (Issue #16), possibly related to ultra-low bandwidth conditions.
2. rBrowser (NomadNet Page Browser)
Repository: github.com/fr33n0w/rBrowser Maturity: Beta Relevance: Limited (content browsing, not terminal)
Architecture
Web-based standalone browser for NomadNet nodes using Flask/Waitress backend.
┌─────────────────────────────────────────────────────────────────┐
│ rBrowser Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Browser │◄──────► │ Flask │ │
│ │ (Web UI) │ HTTP │ Backend │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ │ RNS/NomadNet │
│ ▼ │
│ ┌──────────────┐ │
│ │ NomadNet │ │
│ │ Nodes │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Bidirectional Data Patterns
Request-Response (not streaming):
- Page requests to remote nodes
- Ping for reachability/RTT measurement
- Form submissions with user input
Caching: Local cache of index.mu pages for offline search.
Assessment for Terminal Use: Not applicable. rBrowser is content browsing (request-response), not bidirectional streaming required for terminal I/O.
3. rrcd (Reticulum Relay Chat)
Repository: github.com/kc1awv/rrcd Maturity: Functional/Production Relevance: Medium (chat architecture, not terminal)
Architecture
Hub-spoke IRC-like chat. Ephemeral by design (no message history).
┌─────────────────────────────────────────────────────────────────┐
│ RRC Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client A ◄─────► Hub (rrcd) ◄─────► Client B │
│ │ │
│ │ ◄─────► Client C │
│ │
│ - Link-based identity (remote hash = peer identity) │
│ - Central hub routes/forwards messages │
│ - Commands via "/" prefix (operator/mod functions) │
│ - Large payloads: RNS.Resource for auto-chunking │
│ │
└─────────────────────────────────────────────────────────────────┘
Authentication
Identity-based (not credential-based):
- RNS Link establishment provides remote identity hash
- Hub uses Link’s remote identity as authoritative peer ID
- Operator authority via
trusted_identitiesallowlist in config
"The host running rrcd is trusted by the operator
(if the host is compromised, the hub and its policy controls are compromised)"
Remote Execution Patterns
Command routing (not shell execution):
- Client sends MSG/NOTICE with
/prefix - Hub parses command (e.g.,
/stats,/kick room user) - If sender in
trusted_identities, command executes - All logic centralized on hub server
Assessment for Terminal Use: Limited relevance. RRC is message routing with command parsing, not interactive process I/O. The identity model is instructive.
4. LXMF-Tools: lxmf_terminal
Repository: github.com/SebastianObi/LXMF-Tools Maturity: Beta Relevance: High (LXMF-based terminal)
While not a full shell, lxmf_terminal from LXMF-Tools provides command execution over LXMF:
┌─────────────────────────────────────────────────────────────────┐
│ lxmf_terminal Pattern │
├─────────────────────────────────────────────────────────────────┤
│ │
│ LXMF Client ──────► lxmf_terminal ──────► Local Shell │
│ (Sideband, (LXMF handler) (subprocess) │
│ MeshChat) │
│ │
│ Pattern: Request/Response over LXMF (not streaming) │
│ - Send command as LXMF message │
│ - Execute locally, capture output │
│ - Return output as LXMF message │
│ │
└─────────────────────────────────────────────────────────────────┘
Limitation: Not interactive (no PTY, no continuous I/O). Command-response model only.
Comparison Matrix
| Aspect | rnsh | rBrowser | rrcd | lxmf_terminal |
|---|---|---|---|---|
| Transport | RNS Link/Channel | RNS + HTTP | RNS Link | LXMF |
| Interactive | Yes (PTY) | No | No | No |
| Streaming | Yes | No | No | No |
| Store-forward | No | Via NomadNet | No | Yes |
| Auth Model | Identity hash allowlist | None | Identity hash allowlist | Configurable |
| Latency | Real-time (RTT-bound) | Request/response | Real-time | Delay-tolerant |
| Offline | No (requires Link) | Yes (cached pages) | No (requires Link) | Yes (LXMF queue) |
Styrene Terminal Session Design Options
Option A: Wrap/Integrate rnsh
Approach: Use rnsh as the terminal backend, add Styrene identity/authorization layer.
┌─────────────────────────────────────────────────────────────────┐
│ Styrene + rnsh Integration │
├─────────────────────────────────────────────────────────────────┤
│ │
│ TUI ──► rnsh client ──► RNS Link ──► rnsh listener │
│ │ │ │
│ │ LXMF │ │
│ └──────────────────────────────────────────► styrened │
│ (authorization, session management) │
│ │
└─────────────────────────────────────────────────────────────────┘
Pros:
- Mature, tested implementation
- Handles all PTY complexity
- OpenBSD-ported (quality signal)
Cons:
- Two identity systems (rnsh allowlist + Styrene authorization)
- Two transports (rnsh Links + Styrene LXMF)
- rnsh is Python subprocess, not library import (process coordination overhead)
- Doesn’t use Styrene’s existing wire protocol
Option B: LXMF Control + RNS Link Data (Hybrid)
Approach: Styrene’s original vision. LXMF for session setup/teardown, RNS Link for terminal I/O.
┌─────────────────────────────────────────────────────────────────┐
│ Styrene Hybrid Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Control Plane (LXMF): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ TUI ──► SESSION_START ──► styrened │ │
│ │ ◄── SESSION_READY (link destination) ◄── │ │
│ │ ──► SESSION_END ──► │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Data Plane (RNS Link): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ TUI ◄────── Terminal I/O (Channel messages) ──────► PTY │ │
│ │ WindowSize, StreamData, CommandExited │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Session Lifecycle:
- TUI → Device (LXMF):
SESSION_STARTwith command, terminal config - Device → TUI (LXMF):
SESSION_READYwith RNS destination for data link - TUI → Device (RNS Link): Establish data channel, begin terminal I/O
- Bidirectional (RNS Link): StreamData, WindowSize messages
- Device → TUI (Link):
CommandExitedwith return code - TUI or Device (LXMF):
SESSION_ENDfor cleanup
Pros:
- Consistent with Styrene’s existing LXMF-based fleet management
- Single authorization model (Styrene identity system)
- LXMF provides store-forward for session setup (offline devices can queue)
- Clean separation: control plane = LXMF, data plane = Link
Cons:
- Must implement PTY handling (can borrow from rnsh)
- Two transport types to maintain
- More complex than pure rnsh
Option C: Pure LXMF (Non-Interactive)
Approach: Command execution only, no interactive terminal. Similar to lxmf_terminal.
┌─────────────────────────────────────────────────────────────────┐
│ LXMF-Only Execution │
├─────────────────────────────────────────────────────────────────┤
│ │
│ TUI ──► EXEC(command, args) ──► styrened ──► subprocess │
│ ◄── EXEC_RESULT(exit_code, stdout, stderr) ◄── │
│ │
│ Already implemented in StyreneProtocol (wire-protocol- │
│ migration.md shows EXEC = 0x40, EXEC_RESULT = 0x60) │
│ │
└─────────────────────────────────────────────────────────────────┘
Pros:
- Already implemented in Styrene
- Works over any latency (delay-tolerant)
- Simple implementation
Cons:
- No interactive terminal (no PTY)
- Cannot run interactive programs (vim, htop, etc.)
- Limited to one-shot commands
Recommendations
Immediate (Phase 2 Completion)
Use Option C for fleet management commands. This is already implemented via StyreneMessageType.EXEC and EXEC_RESULT. Sufficient for:
systemctl status/restart/stopnixos-rebuild switch- Log retrieval
- Status checks
Future (Phase 3 Enhancement)
Implement Option B (Hybrid) for interactive terminal access when needed:
- Borrow from rnsh: Adopt message types (WindowSize, StreamData, CommandExited) and PTY handling patterns
- Keep LXMF for control: Session setup/teardown via existing StyreneProtocol
- Add Link data plane: New message types for terminal session:
# Add to StyreneMessageType (in 0x70-0x7F range reserved for terminal)
TERMINAL_START = 0x70 # Request terminal session
TERMINAL_READY = 0x71 # Response with data link destination
TERMINAL_END = 0x72 # Session teardown
# Data plane messages (sent over RNS Link, not LXMF)
class TerminalStreamData:
"""Terminal I/O data (stdin/stdout/stderr)."""
stream: int # 0=stdin, 1=stdout, 2=stderr
data: bytes
class TerminalWindowSize:
"""Terminal resize event."""
rows: int
cols: int
class TerminalExited:
"""Process terminated."""
return_code: int
- Consider rnsh interop: If rnsh is already installed on devices, Styrene could launch rnsh sessions via LXMF coordination without reimplementing PTY handling.
Integration vs. Reimplementation Decision Matrix
| Factor | Integrate rnsh | Reimplement |
|---|---|---|
| Time to market | Fast | Slow |
| Maintenance burden | Shared with rnsh | Full ownership |
| Feature parity | Immediate | Gradual |
| Authorization model | Dual systems | Unified |
| Wire protocol | Incompatible | Native Styrene |
| Offline session setup | No | Yes (LXMF queue) |
Recommendation: Start with Option C (EXEC commands), evaluate user needs for interactive sessions. If required, implement Option B borrowing heavily from rnsh’s PTY/stream handling code.
Appendix: rnsh Code Patterns
Message Registration
# From rnsh/protocol.py
def register_message_types(channel: RNS.Channel):
"""Register all message types with the channel."""
channel.register_message_type(NoopMessage)
channel.register_message_type(WindowSizeMessage)
channel.register_message_type(ExecuteCommandMessage)
channel.register_message_type(StreamDataMessage)
channel.register_message_type(VersionInfoMessage)
channel.register_message_type(ErrorMessage)
channel.register_message_type(CommandExitedMessage)
Session State Timeout
# From rnsh/session.py
def _set_state(self, state: LSState):
"""Set state with RTT-adaptive timeout."""
timeout = max(
self.outlet.rtt * self.timeout_factor,
max(self.outlet.rtt * 2, 10)
)
# Schedule state check after timeout
Stream Handling
# From rnsh/session.py
class StreamDataMessage(RNS.MessageBase):
MSGTYPE = 4
STREAM_STDIN = 0
STREAM_STDOUT = 1
STREAM_STDERR = 2
def __init__(self, stream_id: int, data: bytes):
self.stream_id = stream_id
self.data = data
References
- rnsh GitHub
- rBrowser GitHub
- rrcd GitHub
- LXMF-Tools GitHub
- Reticulum Manual - Links
- Reticulum Manual - Channels
- LXMF Protocol
- Sideband Remote Commands