15 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:
- Wire format compatibility first: All core message types (
HELLO,WELCOME,JOIN,JOINED,PART,PARTED,MSG,NOTICE,PING,PONG,ERROR) are implemented per spec. - 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.
- Bodies are CBOR maps with unsigned integer keys: Not bitmasks, not strings, not "whatever feels right". Unsigned. Integer. Keys.
- 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
-
Sender sends
T_RESOURCE_ENVELOPEmessage: This is a regular RRC envelope (type50) 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) } -
Sender advertises Reticulum Resource: Immediately after sending the envelope, the sender creates an
RNS.Resourceand advertises it over the link. The resource ID in the envelope is for correlation only (currently unused but reserved for future use). -
Receiver accepts or rejects: The receiver may accept the resource (if it recognizes the
kindand the size is acceptable) or reject it (if resources are disabled, size exceeds limits, or no matching expectation exists). -
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 aNOTICEmessage after reconstruction. Used for large announcements."motd": UTF-8 text delivered as aNOTICEmessage, specifically the hub's message-of-the-day. Sent afterWELCOME."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(key0): Hub name (string)B_WELCOME_VER(key1): Hub version (string)B_WELCOME_CAPS(key2): 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 key64) is present andK_NICKis 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/names [room]: Alias for/who
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-tmode./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)+k <key>/-k: Room key/password (must provide key to join)+r/-r: Registered room (read-only; use/registeror/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 (
+nrtby 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 can configure a list of trusted identity hashes. Trusted identities bypass certain checks (currently unused, reserved for future use).
trusted_identities = [
"a1b2c3d4...",
]
This is a hub-local concept and not exposed to clients.
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
Enhanced Compatibility (Recommended)
- Support
T_RESOURCE_ENVELOPE(message type 50) and Reticulum resources - Advertise
CAP_RESOURCE_ENVELOPEin yourHELLOcapabilities if you support resources - Expect hub greeting to arrive via
NOTICEmessages afterWELCOME - Handle chunked
NOTICEmessages (multiple messages with the same content type)
Full Compatibility (Hub Implementers)
- Implement resource transfer with envelope-first protocol
- Keep
WELCOMEminimal (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
- Use resources: If you're sending anything over ~500 bytes, use resource transfer. Your users will thank you.
- Ignore capabilities you don't support: We won't be offended. Much.
- Don't over-engineer: The RRC spec is minimal for a reason. Don't add features just because IRC has them.
- Test with low MTU: If your client works over a 500-byte MTU link, it'll work everywhere.
- 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.