Files
rrcd/EX1-RRCD.md

16 KiB

EX1-RRCD: rrcd Extensions to the RRC Specification

If you're reading this document, you're probably implementing something that needs to talk to rrcd, or you're building your own hub and want to understand what liberties we've taken with the core RRC specification. This is that document.

The RRC specification is intentionally minimal and deliberately vague about certain implementation details, presumably because the authors enjoy watching implementers squirm. We don't hate you quite that much, so we're documenting our extensions here.

Important: If you're implementing a basic RRC client, you can safely ignore most of this document. The core protocol works fine. These extensions are optional capabilities that clients may choose to support (or not).

Philosophy

rrcd implements the core RRC protocol as specified, with the following principles:

  1. Wire format compatibility first: All core message types (HELLO, WELCOME, JOIN, JOINED, PART, PARTED, MSG, NOTICE, PING, PONG, ERROR) are implemented per spec.
  2. Envelope keys are unsigned integers: If you send string keys in your CBOR maps, we will reject your messages. The spec says unsigned integers. We mean it.
  3. Bodies are CBOR maps with unsigned integer keys: Not bitmasks, not strings, not "whatever feels right". Unsigned. Integer. Keys.
  4. Capabilities are advisory: When we advertise capabilities, they're hints. You're allowed to ignore them if you hate yourself.

Extension: Resource Transfer (T_RESOURCE_ENVELOPE)

Message Type: 50 (T_RESOURCE_ENVELOPE)
Capability Key: 0 (CAP_RESOURCE_ENVELOPE)
Status: Implemented (optional, configurable)

The RRC specification has no concept of large message delivery beyond "chunk it yourself, good luck." This is fine for small messages but becomes obnoxious for:

  • Large MOTD/greeting text
  • Binary blobs (theoretically)
  • Anything approaching the link MTU

We added a resource transfer mechanism using Reticulum's built-in RNS.Resource class. This is entirely optional - clients that don't support it will simply not receive messages that exceed MTU. (If you're a client implementer and you want to receive verbose MOTDs, you'll need to support this.)

Protocol Flow

  1. Sender sends T_RESOURCE_ENVELOPE message: This is a regular RRC envelope (type 50) containing metadata about the incoming resource.

    Envelope structure:

    {
        0: 1,              # protocol version (K_V)
        1: 50,             # message type T_RESOURCE_ENVELOPE (K_T)
        2: <8-byte-id>,    # message ID (K_ID)
        3: <timestamp>,    # millisecond timestamp (K_TS)
        4: <sender-hash>,  # sender identity hash (K_SRC)
        5: <room>,         # optional: room name (K_ROOM)
        6: <body>          # body (K_BODY) - see below
    }
    

    Body structure (unsigned integer keys):

    {
        0: <resource-id>,      # B_RES_ID: 8 bytes, unique identifier
        1: <kind>,             # B_RES_KIND: string ("notice", "motd", "blob")
        2: <size>,             # B_RES_SIZE: integer, total bytes
        3: <sha256>,           # B_RES_SHA256: 32 bytes (optional but recommended)
        4: <encoding>          # B_RES_ENCODING: string, e.g. "utf-8" (optional)
    }
    
  2. Sender advertises Reticulum Resource: Immediately after sending the envelope, the sender creates an RNS.Resource and advertises it over the link. The resource ID in the envelope is for correlation only (currently unused but reserved for future use).

  3. Receiver accepts or rejects: The receiver may accept the resource (if it recognizes the kind and the size is acceptable) or reject it (if resources are disabled, size exceeds limits, or no matching expectation exists).

  4. Resource transfer completes: Reticulum handles the chunked transfer. On completion, the receiver verifies the SHA256 hash (if provided) and dispatches the payload based on kind.

Resource Kinds

  • "notice": UTF-8 text delivered as a NOTICE message after reconstruction. Used for large announcements.
  • "motd": UTF-8 text delivered as a NOTICE message, specifically the hub's message-of-the-day. Sent after WELCOME.
  • "blob": Binary data (reserved for future use; currently unused).

Encoding: For text-based kinds (notice, motd), the B_RES_ENCODING field should specify the text encoding (default: "utf-8"). For blob, encoding is irrelevant.

Configuration

Resource transfer is controlled by hub configuration:

enable_resource_transfer = true      # default: true
max_resource_bytes = 262144          # 256 KiB default
max_pending_resource_expectations = 8
resource_expectation_ttl_s = 30.0

Clients: if you don't want to deal with resources, don't advertise CAP_RESOURCE_ENVELOPE in your HELLO. The hub will fall back to chunked NOTICE messages (which may be truncated if they exceed MTU).

Why?

Because the RRC spec doesn't define how to send a 5KB MOTD over a 500-byte MTU link without making everyone cry. That's why.

Extension: WELCOME Minimalism + Greeting-via-NOTICE

The RRC specification is vague about what goes in the WELCOME body. Some implementations send the entire hub greeting, user count, room list, kitchen sink, and a partridge in a pear tree.

We don't do that.

rrcd's WELCOME body contains:

  • B_WELCOME_HUB (key 0): Hub name (string)
  • B_WELCOME_VER (key 1): Hub version (string)
  • B_WELCOME_CAPS (key 2): Capabilities map (optional, currently unused)

That's it. No greeting, no room list, no user count. Why? Because WELCOME needs to fit in a single packet on low-MTU links. If the hub has a greeting configured, it's delivered after WELCOME via one or more NOTICE messages (chunked to fit MTU, or sent via resource transfer if supported).

Client implementers: Don't expect the hub greeting in the WELCOME body. Wait for the NOTICE message(s) that follow. Or don't. We're not your supervisor.

Extension: HELLO Legacy Nickname Field

Body Key: 64 (B_HELLO_NICK_LEGACY)
Status: Deprecated, supported for compatibility

Some pre-specification implementations sent the client nickname in the HELLO body under key 64. This is deprecated. The RRC spec defines envelope-level nickname field (K_NICK, key 7), which should be used instead.

rrcd supports both:

  • If K_NICK (envelope-level) is present, use it.
  • If B_HELLO_NICK_LEGACY (body key 64) is present and K_NICK is absent, fall back to it.

Client implementers: Use K_NICK (envelope key 7). Don't use body key 64 unless you enjoy living in the past.

Extension: Hub Commands

The RRC specification has no concept of "hub commands" or "slash commands" beyond what individual implementations invent. rrcd implements a set of IRC-style commands for room and hub management.

How it works: Any MSG message sent to a room (or without a room field) that starts with / is interpreted as a command. If the command is recognized, it's handled by the hub and not forwarded to the room. If unrecognized, it's forwarded as a normal chat message (so you can still say "/shrug" without triggering a command parser meltdown).

Global/Hub Commands

These work from any room (or no room):

  • /reload: Reload hub configuration (server operator only)
  • /stats: Display hub statistics (server operator only)
  • /who [room]: List members in a room. Private rooms (+p) are hidden from non-operators.
  • /names [room]: Alias for /who
  • /list: List all registered public rooms with their topics. Excludes private rooms (+p) and ephemeral (non-registered) rooms.

Room Management Commands

Room founders and operators can use these:

  • /register <room>: Register a room (makes it persistent). Founder only, must be present in room.
  • /unregister <room>: Unregister a room. Founder only, must be present in room.
  • /topic <room> [text]: View or set room topic. Operators can always set; regular users can set if -t mode.
  • /mode <room> <flag>: Set room modes (see below).

Moderation Commands

  • /kick <room> <nick|hashprefix>: Remove a user from a room (operator only)
  • /kline add|del|list [hash]: Global ban by identity hash (server operator only)
  • /ban <room> add|del|list [hash]: Room-specific ban (operator only)
  • /invite <room> add|del|list [hash]: Manage invite list for invite-only rooms (operator only)
  • /op <room> <nick|hashprefix>: Grant operator status (operator only)
  • /deop <room> <nick|hashprefix>: Remove operator status (operator only, cannot deop founder)
  • /voice <room> <nick|hashprefix>: Grant voice in moderated rooms (operator only)
  • /devoice <room> <nick|hashprefix>: Remove voice (operator only)

Room Modes

IRC-style mode flags (set via /mode <room> <flag>):

  • +m / -m: Moderated (only voiced/ops can speak)
  • +i / -i: Invite-only (must be invited to join)
  • +t / -t: Topic protected (only operators can set topic)
  • +n / -n: No outside messages (must be in room to send messages)
  • +p / -p: Private room (hidden from /list command and /who for non-operators)
  • +k <key> / -k: Room key/password (must provide key to join)
  • +r / -r: Registered room (read-only; use /register or /unregister)
  • +o <hash> / -o <hash>: Grant/remove operator status
  • +v <hash> / -v <hash>: Grant/remove voice

Note: These commands and modes are not part of the RRC specification. They are rrcd-specific and hub-local. Other hubs may implement entirely different command sets (or none at all). Clients should not assume these commands exist.

Extension: Room Registry and Persistence

The RRC specification says nothing about persistent rooms. rrcd implements a room registry system that persists room state (modes, operators, bans, topic, etc.) to disk.

Registry file: ~/.rrcd/rooms.toml (configurable via room_registry_path)

Registered rooms:

  • Survive hub restarts
  • Retain operators, bans, topic, modes
  • Can be configured with default modes (+nrt by default)
  • Are pruned if unused for a configurable period (default: 30 days)

Unregistered rooms:

  • Exist only while members are present
  • Ephemeral state (lost on last member departure)

Founder: The first person to create a room is the founder. Only the founder can register or unregister the room. Founders cannot be de-opped.

This is entirely hub-local and transparent to clients. Clients don't need to do anything special.

Extension: Invite Timeout

When a room is invite-only (+i), operators can add users to the invite list via /invite <room> add <hash>. Invites have a configurable timeout (default: 900 seconds / 15 minutes).

This prevents the invite list from growing unbounded. Expired invites are pruned periodically.

Configuration:

room_invite_timeout_s = 900.0

Extension: Nickname Normalization

The RRC spec says nicknames are "advisory" and may be "ridiculous." rrcd normalizes nicknames:

  • Maximum length: configurable (default: 32 characters)
  • Leading/trailing whitespace stripped
  • Control characters rejected
  • Empty nicknames rejected

If a nickname fails validation, it's rejected and the user is assigned no nickname (hash-only identification).

Configuration:

nick_max_chars = 32

Extension: Rate Limiting

To prevent abuse, rrcd implements per-session rate limiting using a token bucket algorithm.

Default: 240 messages per minute
Configuration:

rate_limit_msgs_per_minute = 240

If a client exceeds the rate limit, excess messages are dropped (not queued). The client is not disconnected or notified. This is intentional: rate limits are for abuse prevention, not chat flow control.

Extension: Ping/Pong Timeout

The RRC spec defines PING and PONG messages but doesn't specify timeout behavior. rrcd allows configurable ping intervals and timeouts:

ping_interval_s = 0.0    # 0 = disabled
ping_timeout_s = 0.0     # 0 = no timeout

If enabled, the hub sends PING periodically. If a client fails to respond with PONG within the timeout, the connection is terminated.

Default: Disabled (because Reticulum already has link-level keepalives).

Extension: Trusted Identities (Server Operators)

Server operators can configure a list of trusted identity hashes. Trusted identities are granted server operator privileges, allowing them to execute administrative commands.

trusted_identities = [
    "a1b2c3d4e5f67890abcdef...",  # full 32-byte identity hash in hex
]

Server operator commands (requires trusted identity):

  • /reload - Reload hub configuration and room registry
  • /who <room> - List members in a room if it exists
  • /stats - View hub statistics (messages, bytes, resources, sessions)
  • /kline - Global ban management (add/del/list)

Implementation: The hub checks the peer's identity hash against the trusted_identities list. If a non-trusted user attempts a server operator command, they receive an ERROR message with "not authorized" and the command is rejected.

Security note: Server operators also have implicit room operator status in all rooms, allowing them to moderate any room without being explicitly granted +o status.

This is a hub-local concept and not exposed to clients (no capability flag or protocol message).

Extension: Banned Identities (K-Lines)

Server operators can ban identity hashes globally via /kline commands or configuration:

banned_identities = [
    "deadbeef...",
]

Banned identities are rejected at connection establishment (before HELLO).

Extension: Statistics Tracking

rrcd tracks various counters (messages sent/received, bytes in/out, resources transferred, etc.). Server operators can view stats via /stats.

This is hub-local and not exposed to regular users.

What We Deliberately Did NOT Extend

Some things we intentionally did not add, despite IRC implementing them:

  • Channel services (ChanServ, NickServ, etc.): Not needed. Identity hashes are cryptographically unique. Use them.
  • Server-to-server linking: RRC is designed for single-hub deployments over Reticulum. Federating hubs is out of scope.
  • DCC/file transfer: Use Reticulum's file transfer mechanisms directly if you need them.
  • Flood protection beyond rate limiting: We rate limit. If you're getting flooded, ban the offender.

For Implementers: Compatibility Checklist

If you're implementing a client or another hub, here's what you need to know:

Minimum Compatibility (Basic RRC Client)

  • Implement core message types (HELLO, WELCOME, JOIN, JOINED, PART, PARTED, MSG, NOTICE, PING, PONG, ERROR)
  • Use CBOR encoding
  • Use unsigned integer keys in envelopes and bodies
  • Handle K_NICK (envelope key 7) for nicknames
  • Gracefully ignore unknown message types
  • Support T_RESOURCE_ENVELOPE (message type 50) and Reticulum resources
  • Advertise CAP_RESOURCE_ENVELOPE in your HELLO capabilities if you support resources
  • Expect hub greeting to arrive via NOTICE messages after WELCOME
  • Handle chunked NOTICE messages (multiple messages with the same content type)

Full Compatibility (Hub Implementers)

  • Implement resource transfer with envelope-first protocol
  • Keep WELCOME minimal (hub name, version, caps only)
  • Chunk large messages or use resources to stay within MTU
  • Support legacy B_HELLO_NICK_LEGACY (body key 64) for old clients
  • Normalize and validate nicknames before accepting them
  • Implement rate limiting to prevent abuse
  • Consider implementing room persistence (optional)

Non-Normative Advice for the Weary

  1. Use resources: If you're sending anything over ~500 bytes, use resource transfer. Your users will thank you.
  2. Ignore capabilities you don't support: We won't be offended. Much.
  3. Don't over-engineer: The RRC spec is minimal for a reason. Don't add features just because IRC has them.
  4. Test with low MTU: If your client works over a 500-byte MTU link, it'll work everywhere.
  5. Read the rrcd source: If this doc is unclear, the code is (arguably) clearer. Or at least executable.

If you find a bug, inconsistency, or deeply offensive opinion in this document, file an issue. Or don't. We'll probably find it eventually.