mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-04-25 21:19:58 -07:00
add suggested reading section and new document for rrcd extensions
This commit is contained in:
406
EX1-RRCD.md
Normal file
406
EX1-RRCD.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# 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**:
|
||||
```python
|
||||
{
|
||||
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):
|
||||
```python
|
||||
{
|
||||
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:
|
||||
|
||||
```toml
|
||||
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
|
||||
- `/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 `-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)
|
||||
- `+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**:
|
||||
```toml
|
||||
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**:
|
||||
```toml
|
||||
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**:
|
||||
```toml
|
||||
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:
|
||||
|
||||
```toml
|
||||
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).
|
||||
|
||||
```toml
|
||||
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:
|
||||
|
||||
```toml
|
||||
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_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.*
|
||||
Reference in New Issue
Block a user