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

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):

TypeIDPurpose
NoopMessage0Keep-alive
WindowSizeMessage2Terminal dimensions (rows, cols, pixels)
ExecuteCommandMessage3Launch command with args, TTY config
StreamDataMessage4stdin (0), stdout (1), stderr (2) data
VersionInfoMessage5Protocol handshake
ErrorMessage6Error with fatal flag
CommandExitedMessage7Process 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:

  1. Identity-based: Validates against allowlist of destination hashes (-a HASH)
  2. No-auth: Open access with -n flag
  3. File-based: Reads allowed identities from file, hot-reloadable

Remote identity passed to child process via RNS_REMOTE_IDENTITY environment variable.

Command Execution Policies

FlagBehavior
(none)Initiator command preferred, fallback to listener config, then default shell
-AAppend initiator command to listener command
-CReject 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_identities allowlist 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):

  1. Client sends MSG/NOTICE with / prefix
  2. Hub parses command (e.g., /stats, /kick room user)
  3. If sender in trusted_identities, command executes
  4. 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

AspectrnshrBrowserrrcdlxmf_terminal
TransportRNS Link/ChannelRNS + HTTPRNS LinkLXMF
InteractiveYes (PTY)NoNoNo
StreamingYesNoNoNo
Store-forwardNoVia NomadNetNoYes
Auth ModelIdentity hash allowlistNoneIdentity hash allowlistConfigurable
LatencyReal-time (RTT-bound)Request/responseReal-timeDelay-tolerant
OfflineNo (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

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:

  1. TUI → Device (LXMF): SESSION_START with command, terminal config
  2. Device → TUI (LXMF): SESSION_READY with RNS destination for data link
  3. TUI → Device (RNS Link): Establish data channel, begin terminal I/O
  4. Bidirectional (RNS Link): StreamData, WindowSize messages
  5. Device → TUI (Link): CommandExited with return code
  6. TUI or Device (LXMF): SESSION_END for 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/stop
  • nixos-rebuild switch
  • Log retrieval
  • Status checks

Future (Phase 3 Enhancement)

Implement Option B (Hybrid) for interactive terminal access when needed:

  1. Borrow from rnsh: Adopt message types (WindowSize, StreamData, CommandExited) and PTY handling patterns
  2. Keep LXMF for control: Session setup/teardown via existing StyreneProtocol
  3. 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
  1. 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

FactorIntegrate rnshReimplement
Time to marketFastSlow
Maintenance burdenShared with rnshFull ownership
Feature parityImmediateGradual
Authorization modelDual systemsUnified
Wire protocolIncompatibleNative Styrene
Offline session setupNoYes (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

Graph