mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-06-12 07:43:31 -07:00
Merge pull request #7 from kc1awv/resource_transfer
add resource transfer and patch small bugs
This commit is contained in:
+15
-4
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user