Merge pull request #7 from kc1awv/resource_transfer

add resource transfer and patch small bugs
This commit is contained in:
Steve Miller
2026-01-01 15:52:11 -05:00
committed by GitHub
9 changed files with 935 additions and 66 deletions
+15 -4
View File
@@ -2,6 +2,21 @@
This project follows the versioning policy in VERSIONING.md.
## 0.1.2 - 2026-01-01
- Implemented RNS.Resource transfer for messages exceeding MTU limits, with resource envelope handling and automatic fallback
- Allow hub-directed commands (e.g., `/stats`, `/reload`, `/who`, `/kline`) to be sent without a room field
- Removed validation that rejected empty room fields in envelopes, per RRC specification
- Hub-level commands now send responses with no room field (`room=None`) for better client compatibility
- Refactored greeting messages to use dedicated MOTD resource kind for clearer semantics
- Added missing configuration options to default config template
## 0.1.1 - 2025-12-30
- Protocol extension: hub may attach an optional nickname (`K_NICK = 7`) to forwarded `MSG`/`NOTICE` envelopes for improved user identification
## 0.1.0 - 2025-12-29
Initial public release.
@@ -13,7 +28,3 @@ Initial public release.
- Persistent config + room registry in TOML (`rrcd.toml`, `rooms.toml`)
- Reduced lock contention by flushing outbound packets outside the shared state lock
- Added small packaging metadata and README polish
## 0.1.1 - 2025-12-30
- Protocol extension: hub may attach an optional nickname (`K_NICK = 7`) to forwarded `MSG`/`NOTICE` envelopes based on the nickname provided in `HELLO`.
+34
View File
@@ -139,6 +139,40 @@ Wire-level extensions (backwards-compatible):
UTF-8 encodable, contain no newlines/NUL, and are at most `nick_max_chars`
characters (default: 32).
- **Large payload transfer via RNS.Resource**: For messages that exceed the link
MTU (Maximum Data Unit), `rrcd` can automatically use RNS.Resource for
reliable large payload transfer instead of manual chunking.
This is implemented as a two-part protocol:
1. Send a small `RESOURCE_ENVELOPE` message (type 50) via normal packet,
announcing the incoming resource with metadata (id, kind, size, SHA256).
2. Send the actual payload via `RNS.Resource`.
The receiving side matches the resource to the expectation and validates
integrity. Supported resource kinds include:
- `notice`: Large NOTICE text messages
- `motd`: Message of the day / server greeting
- `blob`: Generic binary data
Configuration (in `rrcd.toml`):
```toml
[hub]
enable_resource_transfer = true # default: true
max_resource_bytes = 262144 # 256 KiB default
max_pending_resource_expectations = 8 # per link
resource_expectation_ttl_s = 30.0 # expectation timeout
```
Safety controls:
- Resources are only accepted if they match a recent expectation
- Size limits enforced (default 256 KiB)
- SHA256 verification for integrity
- TTL-based expectation expiry (default 30 seconds)
- Per-link expectation limit to prevent memory exhaustion
Fallback: If resource transfer is disabled or fails, NOTICE messages fall
back to the original line-based chunking method.
Configure trusted operators and banned identities in the TOML config:
- `trusted_identities`: list of Reticulum Identity hashes (hex) allowed to run
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rrcd"
version = "0.1.1"
version = "0.1.2"
description = "Reticulum Relay Chat daemon (hub service)"
readme = "README.md"
license = { file = "LICENSE" }
+17 -2
View File
@@ -121,8 +121,8 @@ announce_period_s = 0.0
hub_name = "rrc"
greeting = ""
# Note: The hub greeting is delivered after WELCOME via one or more NOTICE
# messages. NOTICE payloads are chunked as needed to fit the Link MTU.
# Note: The hub 'greeting' is the MOTD (message of the day) delivered after WELCOME.
# If it exceeds the link MTU, it will be sent via RNS.Resource for reliable transfer.
# Operator / moderation
#
@@ -158,6 +158,21 @@ rate_limit_msgs_per_minute = 240
ping_interval_s = 0.0
ping_timeout_s = 0.0
# Large payload transfer via RNS.Resource
#
# When a message exceeds the link MTU, rrcd can use RNS.Resource for reliable
# transfer instead of manual chunking. A small RESOURCE_ENVELOPE is sent first,
# followed by the payload as an RNS.Resource.
#
# enable_resource_transfer: enable/disable feature (default: true)
# max_resource_bytes: maximum size for a single resource (default: 256 KiB)
# max_pending_resource_expectations: max pending expectations per link (default: 8)
# resource_expectation_ttl_s: how long to wait for announced resource (default: 30s)
enable_resource_transfer = true
max_resource_bytes = 262144
max_pending_resource_expectations = 8
resource_expectation_ttl_s = 30.0
[logging]
# Log level for rrcd itself.
+4
View File
@@ -26,6 +26,10 @@ class HubRuntimeConfig:
rate_limit_msgs_per_minute: int = 240
ping_interval_s: float = 0.0
ping_timeout_s: float = 0.0
max_resource_bytes: int = 256 * 1024 # 256 KiB default
max_pending_resource_expectations: int = 8
resource_expectation_ttl_s: float = 30.0
enable_resource_transfer: bool = True
log_level: str = "INFO"
log_rns_level: str = "WARNING"
log_console: bool = True
+14
View File
@@ -29,6 +29,8 @@ T_PONG = 31
T_ERROR = 40
T_RESOURCE_ENVELOPE = 50
# HELLO body keys
# Per spec: key assignments are fixed.
B_HELLO_NAME = 0
@@ -46,3 +48,15 @@ B_WELCOME_CAPS = 2
# Capabilities map keys (values are advisory). Keep these small and numeric.
CAP_RESOURCE_ENVELOPE = 0
# RESOURCE_ENVELOPE body keys
B_RES_ID = 0
B_RES_KIND = 1
B_RES_SIZE = 2
B_RES_SHA256 = 3
B_RES_ENCODING = 4
# Resource kinds (string values)
RES_KIND_NOTICE = "notice"
RES_KIND_MOTD = "motd"
RES_KIND_BLOB = "blob"
+1 -2
View File
@@ -85,8 +85,7 @@ def validate_envelope(env: dict) -> None:
room = env[K_ROOM]
if not isinstance(room, str):
raise TypeError("room name must be a string")
if room == "":
raise ValueError("room name must not be empty")
# Per RRC spec, room field may be empty (e.g., for hub commands)
if K_NICK in env:
nick = env[K_NICK]
+752 -57
View File
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
"""Tests for resource transfer functionality."""
import hashlib
import os
from rrcd.codec import decode, encode
from rrcd.constants import (
B_RES_ENCODING,
B_RES_ID,
B_RES_KIND,
B_RES_SHA256,
B_RES_SIZE,
K_BODY,
K_SRC,
K_T,
RES_KIND_NOTICE,
T_RESOURCE_ENVELOPE,
)
from rrcd.envelope import make_envelope
def test_resource_envelope_serialization():
"""Test that resource envelopes can be created and serialized."""
src = os.urandom(16)
rid = os.urandom(8)
payload = b"This is a test payload that is larger than typical MDU"
sha256 = hashlib.sha256(payload).digest()
body = {
B_RES_ID: rid,
B_RES_KIND: RES_KIND_NOTICE,
B_RES_SIZE: len(payload),
B_RES_SHA256: sha256,
B_RES_ENCODING: "utf-8",
}
envelope = make_envelope(
T_RESOURCE_ENVELOPE,
src=src,
room="test",
body=body,
)
# Serialize and deserialize
encoded = encode(envelope)
decoded = decode(encoded)
assert decoded[K_T] == T_RESOURCE_ENVELOPE
assert decoded[K_SRC] == src
decoded_body = decoded[K_BODY]
assert decoded_body[B_RES_ID] == rid
assert decoded_body[B_RES_KIND] == RES_KIND_NOTICE
assert decoded_body[B_RES_SIZE] == len(payload)
assert decoded_body[B_RES_SHA256] == sha256
assert decoded_body[B_RES_ENCODING] == "utf-8"
def test_resource_envelope_minimal():
"""Test resource envelope with minimal required fields."""
src = os.urandom(16)
rid = os.urandom(8)
body = {
B_RES_ID: rid,
B_RES_KIND: "blob",
B_RES_SIZE: 1024,
}
envelope = make_envelope(
T_RESOURCE_ENVELOPE,
src=src,
body=body,
)
encoded = encode(envelope)
decoded = decode(encoded)
decoded_body = decoded[K_BODY]
assert B_RES_SHA256 not in decoded_body
assert B_RES_ENCODING not in decoded_body
assert decoded_body[B_RES_SIZE] == 1024
def test_sha256_verification():
"""Test SHA256 hash computation for payload verification."""
payload = b"Test payload for SHA256 verification"
expected = hashlib.sha256(payload).digest()
# Verify we can compute and compare hashes correctly
computed = hashlib.sha256(payload).digest()
assert computed == expected
assert len(computed) == 32
# Verify mismatch detection
wrong_payload = b"Different payload"
wrong_hash = hashlib.sha256(wrong_payload).digest()
assert wrong_hash != expected