mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-05-11 19:31:17 -07:00
align protocol constants and update handling of HELLO/WELCOME messages
This commit is contained in:
23
README.md
23
README.md
@@ -80,15 +80,22 @@ too large for the current Reticulum link MTU.
|
|||||||
|
|
||||||
Mitigations:
|
Mitigations:
|
||||||
|
|
||||||
- Keep `greeting` reasonably short if you want it to appear inside `WELCOME`.
|
- `WELCOME` sent by `rrcd` is intentionally minimal (hub name/version/caps).
|
||||||
- If `WELCOME` would exceed MTU, `rrcd` automatically sends a minimal `WELCOME`
|
- The hub `greeting` is delivered after `WELCOME` via one or more `NOTICE`
|
||||||
and then delivers the full greeting as one or more `NOTICE` messages sized to
|
messages chunked to fit the link MTU.
|
||||||
fit the link MTU.
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
`rrcd` implements the core RRC protocol as described in the RRC docs.
|
`rrcd` implements the core RRC protocol as described in the RRC docs.
|
||||||
|
|
||||||
|
Protocol alignment notes (for implementers):
|
||||||
|
|
||||||
|
- `HELLO` and `WELCOME` bodies are CBOR maps with unsigned integer keys.
|
||||||
|
- Capabilities are carried in body key `2` as a CBOR map (not a bitmask). Keys
|
||||||
|
inside the capabilities map are unsigned integers; values are advisory.
|
||||||
|
- `WELCOME` is intentionally minimal (hub name/version/caps only). Any hub
|
||||||
|
greeting text is delivered after `WELCOME` via one or more `NOTICE` messages.
|
||||||
|
|
||||||
Extensions beyond core RRC will be documented in the Extensions section of this
|
Extensions beyond core RRC will be documented in the Extensions section of this
|
||||||
README as they are added.
|
README as they are added.
|
||||||
|
|
||||||
@@ -120,10 +127,10 @@ Wire-level extensions (backwards-compatible):
|
|||||||
hint associated with `K_SRC` so clients can display a human-friendly
|
hint associated with `K_SRC` so clients can display a human-friendly
|
||||||
nickname instead of only the sender identity hash.
|
nickname instead of only the sender identity hash.
|
||||||
|
|
||||||
The hub learns this value from the client's `HELLO` body key
|
The hub learns this value from the client's optional envelope nickname field
|
||||||
`B_HELLO_NICK = 0` and treats it as the authoritative nickname for that
|
`K_NICK = 7` on inbound messages, and treats it as the authoritative nickname
|
||||||
link. Clients should treat `K_NICK` as optional and fall back to `K_SRC`
|
for that link. Clients should treat `K_NICK` as optional and fall back to
|
||||||
when it is missing.
|
`K_SRC` when it is missing.
|
||||||
|
|
||||||
Nicknames are advisory only; clients should treat them as display hints.
|
Nicknames are advisory only; clients should treat them as display hints.
|
||||||
The hub may ignore, sanitize, replace, or omit them.
|
The hub may ignore, sanitize, replace, or omit them.
|
||||||
|
|||||||
14
rrcd/cli.py
14
rrcd/cli.py
@@ -117,14 +117,12 @@ dest_name = "rrc.hub"
|
|||||||
announce_on_start = true
|
announce_on_start = true
|
||||||
announce_period_s = 0.0
|
announce_period_s = 0.0
|
||||||
|
|
||||||
# WELCOME message fields.
|
# Hub identity fields.
|
||||||
hub_name = "rrc"
|
hub_name = "rrc"
|
||||||
greeting = ""
|
greeting = ""
|
||||||
|
|
||||||
# Note: Some Reticulum links have low MTU. If `greeting` is very long, the hub
|
# Note: The hub greeting is delivered after WELCOME via one or more NOTICE
|
||||||
# may be unable to include it inside the initial WELCOME. In that case, rrcd
|
# messages. NOTICE payloads are chunked as needed to fit the Link MTU.
|
||||||
# will send a minimal WELCOME and then deliver the greeting afterward via NOTICE
|
|
||||||
# messages.
|
|
||||||
|
|
||||||
# Operator / moderation
|
# Operator / moderation
|
||||||
#
|
#
|
||||||
@@ -296,7 +294,11 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|||||||
)
|
)
|
||||||
|
|
||||||
p.add_argument("--hub-name", default=None, help="Hub name in WELCOME")
|
p.add_argument("--hub-name", default=None, help="Hub name in WELCOME")
|
||||||
p.add_argument("--greeting", default=None, help="Greeting in WELCOME")
|
p.add_argument(
|
||||||
|
"--greeting",
|
||||||
|
default=None,
|
||||||
|
help="Greeting delivered via NOTICE after WELCOME",
|
||||||
|
)
|
||||||
p.add_argument(
|
p.add_argument(
|
||||||
"--include-joined-member-list",
|
"--include-joined-member-list",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
@@ -30,12 +30,19 @@ T_PONG = 31
|
|||||||
T_ERROR = 40
|
T_ERROR = 40
|
||||||
|
|
||||||
# HELLO body keys
|
# HELLO body keys
|
||||||
B_HELLO_NICK = 0
|
# Per spec: key assignments are fixed.
|
||||||
B_HELLO_NAME = 1
|
B_HELLO_NAME = 0
|
||||||
B_HELLO_VER = 2
|
B_HELLO_VER = 1
|
||||||
B_HELLO_CAPS = 3
|
B_HELLO_CAPS = 2
|
||||||
|
|
||||||
|
# Legacy / pre-spec implementations may have sent nick in HELLO body.
|
||||||
|
# Prefer the envelope-level nickname field (K_NICK=7) going forward.
|
||||||
|
B_HELLO_NICK_LEGACY = 64
|
||||||
|
|
||||||
# WELCOME body keys
|
# WELCOME body keys
|
||||||
B_WELCOME_HUB = 0
|
B_WELCOME_HUB = 0
|
||||||
B_WELCOME_GREETING = 1
|
B_WELCOME_VER = 1
|
||||||
B_WELCOME_CAPS = 2
|
B_WELCOME_CAPS = 2
|
||||||
|
|
||||||
|
# Capabilities map keys (values are advisory). Keep these small and numeric.
|
||||||
|
CAP_RESOURCE_ENVELOPE = 0
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ from . import __version__
|
|||||||
from .codec import decode, encode
|
from .codec import decode, encode
|
||||||
from .config import HubRuntimeConfig
|
from .config import HubRuntimeConfig
|
||||||
from .constants import (
|
from .constants import (
|
||||||
B_HELLO_NICK,
|
B_HELLO_CAPS,
|
||||||
B_WELCOME_GREETING,
|
B_HELLO_NICK_LEGACY,
|
||||||
B_WELCOME_HUB,
|
B_WELCOME_HUB,
|
||||||
|
B_WELCOME_VER,
|
||||||
|
B_WELCOME_CAPS,
|
||||||
K_BODY,
|
K_BODY,
|
||||||
K_NICK,
|
K_NICK,
|
||||||
K_ROOM,
|
K_ROOM,
|
||||||
@@ -101,6 +103,12 @@ class HubService:
|
|||||||
"announces": 0,
|
"announces": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _extract_caps(self, body: Any) -> dict[int, Any]:
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
return {}
|
||||||
|
caps = body.get(B_HELLO_CAPS)
|
||||||
|
return caps if isinstance(caps, dict) else {}
|
||||||
|
|
||||||
def _fmt_hash(self, h: Any, *, prefix: int = 12) -> str:
|
def _fmt_hash(self, h: Any, *, prefix: int = 12) -> str:
|
||||||
if isinstance(h, (bytes, bytearray)):
|
if isinstance(h, (bytes, bytearray)):
|
||||||
s = bytes(h).hex()
|
s = bytes(h).hex()
|
||||||
@@ -185,43 +193,31 @@ class HubService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
g = str(greeting) if greeting else ""
|
g = str(greeting) if greeting else ""
|
||||||
|
body_w: dict[int, Any] = {
|
||||||
body_w: dict[int, Any] = {B_WELCOME_HUB: self.config.hub_name}
|
B_WELCOME_HUB: self.config.hub_name,
|
||||||
if g:
|
B_WELCOME_VER: str(__version__),
|
||||||
body_w[B_WELCOME_GREETING] = g
|
}
|
||||||
|
# Capabilities are optional; keep WELCOME minimal unless needed.
|
||||||
|
|
||||||
welcome = make_envelope(T_WELCOME, src=self.identity.hash, body=body_w)
|
welcome = make_envelope(T_WELCOME, src=self.identity.hash, body=body_w)
|
||||||
welcome_payload = encode(welcome)
|
welcome_payload = encode(welcome)
|
||||||
|
|
||||||
if self._packet_would_fit(link, welcome_payload):
|
if not self._packet_would_fit(link, welcome_payload):
|
||||||
self._queue_payload(outgoing, link, welcome_payload)
|
|
||||||
self.log.debug(
|
|
||||||
"Queued WELCOME (with greeting) peer=%s link_id=%s",
|
|
||||||
self._fmt_hash(peer_hash),
|
|
||||||
self._fmt_link_id(link),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fallback: send a minimal WELCOME, then send the greeting as NOTICE
|
|
||||||
# chunks that each fit within the link MTU.
|
|
||||||
body_min: dict[int, Any] = {B_WELCOME_HUB: self.config.hub_name}
|
|
||||||
welcome_min = make_envelope(T_WELCOME, src=self.identity.hash, body=body_min)
|
|
||||||
welcome_min_payload = encode(welcome_min)
|
|
||||||
|
|
||||||
if not self._packet_would_fit(link, welcome_min_payload):
|
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"WELCOME would not fit MTU even without greeting; cannot welcome peer=%s link_id=%s",
|
"WELCOME would not fit MTU; cannot welcome peer=%s link_id=%s",
|
||||||
self._fmt_hash(peer_hash),
|
self._fmt_hash(peer_hash),
|
||||||
self._fmt_link_id(link),
|
self._fmt_link_id(link),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log.warning(
|
self._queue_payload(outgoing, link, welcome_payload)
|
||||||
"WELCOME too large for MTU; sending minimal WELCOME + NOTICE chunks peer=%s link_id=%s",
|
self.log.debug(
|
||||||
|
"Queued WELCOME peer=%s link_id=%s",
|
||||||
self._fmt_hash(peer_hash),
|
self._fmt_hash(peer_hash),
|
||||||
self._fmt_link_id(link),
|
self._fmt_link_id(link),
|
||||||
)
|
)
|
||||||
self._queue_payload(outgoing, link, welcome_min_payload)
|
|
||||||
|
# The hub greeting is delivered as NOTICE after WELCOME.
|
||||||
if g:
|
if g:
|
||||||
self._queue_notice_chunks(outgoing, link, room=None, text=g)
|
self._queue_notice_chunks(outgoing, link, room=None, text=g)
|
||||||
|
|
||||||
@@ -2133,6 +2129,7 @@ class HubService:
|
|||||||
"rooms": set(),
|
"rooms": set(),
|
||||||
"peer": None,
|
"peer": None,
|
||||||
"nick": None,
|
"nick": None,
|
||||||
|
"peer_caps": {},
|
||||||
"awaiting_pong": None,
|
"awaiting_pong": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2196,7 +2193,8 @@ class HubService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
sess["welcomed"] = True
|
sess["welcomed"] = True
|
||||||
# Prefer the queued WELCOME path so we can preflight MTU sizing.
|
# Use the queued path so we can preflight MTU sizing and optionally
|
||||||
|
# follow up with NOTICE chunks (e.g. greeting).
|
||||||
outgoing: list[tuple[RNS.Link, bytes]] = []
|
outgoing: list[tuple[RNS.Link, bytes]] = []
|
||||||
self._queue_welcome(
|
self._queue_welcome(
|
||||||
outgoing,
|
outgoing,
|
||||||
@@ -2205,8 +2203,16 @@ class HubService:
|
|||||||
greeting=self.config.greeting,
|
greeting=self.config.greeting,
|
||||||
)
|
)
|
||||||
for out_link, payload in outgoing:
|
for out_link, payload in outgoing:
|
||||||
|
self._inc("bytes_out", len(payload))
|
||||||
try:
|
try:
|
||||||
RNS.Packet(out_link, payload).send()
|
RNS.Packet(out_link, payload).send()
|
||||||
|
except OSError as e:
|
||||||
|
self.log.warning(
|
||||||
|
"Send failed link_id=%s bytes=%s err=%s",
|
||||||
|
self._fmt_link_id(out_link),
|
||||||
|
len(payload),
|
||||||
|
e,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Send failed link_id=%s bytes=%s",
|
"Send failed link_id=%s bytes=%s",
|
||||||
@@ -2308,6 +2314,7 @@ class HubService:
|
|||||||
self._on_packet_locked(link, data, outgoing)
|
self._on_packet_locked(link, data, outgoing)
|
||||||
|
|
||||||
for out_link, payload in outgoing:
|
for out_link, payload in outgoing:
|
||||||
|
self._inc("bytes_out", len(payload))
|
||||||
try:
|
try:
|
||||||
RNS.Packet(out_link, payload).send()
|
RNS.Packet(out_link, payload).send()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -2379,6 +2386,7 @@ class HubService:
|
|||||||
t = env.get(K_T)
|
t = env.get(K_T)
|
||||||
room = env.get(K_ROOM)
|
room = env.get(K_ROOM)
|
||||||
body = env.get(K_BODY)
|
body = env.get(K_BODY)
|
||||||
|
nick = env.get(K_NICK)
|
||||||
|
|
||||||
if self.log.isEnabledFor(logging.DEBUG):
|
if self.log.isEnabledFor(logging.DEBUG):
|
||||||
body_len = None
|
body_len = None
|
||||||
@@ -2408,12 +2416,21 @@ class HubService:
|
|||||||
self._emit_error(outgoing, link, src=self.identity.hash, text="send HELLO first")
|
self._emit_error(outgoing, link, src=self.identity.hash, text="send HELLO first")
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(body, dict):
|
if isinstance(nick, str):
|
||||||
nick = body.get(B_HELLO_NICK)
|
|
||||||
n = normalize_nick(nick, max_chars=self.config.nick_max_chars)
|
n = normalize_nick(nick, max_chars=self.config.nick_max_chars)
|
||||||
if n is not None:
|
if n is not None:
|
||||||
sess["nick"] = n
|
sess["nick"] = n
|
||||||
|
|
||||||
|
if isinstance(body, dict):
|
||||||
|
sess["peer_caps"] = self._extract_caps(body)
|
||||||
|
|
||||||
|
# Back-compat: if a legacy client put nick in HELLO body, accept it.
|
||||||
|
if sess.get("nick") is None:
|
||||||
|
legacy_nick = body.get(B_HELLO_NICK_LEGACY)
|
||||||
|
n2 = normalize_nick(legacy_nick, max_chars=self.config.nick_max_chars)
|
||||||
|
if n2 is not None:
|
||||||
|
sess["nick"] = n2
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"HELLO peer=%s nick=%r link_id=%s",
|
"HELLO peer=%s nick=%r link_id=%s",
|
||||||
self._fmt_hash(peer_hash),
|
self._fmt_hash(peer_hash),
|
||||||
@@ -2438,14 +2455,22 @@ class HubService:
|
|||||||
sess["welcomed"] = False
|
sess["welcomed"] = False
|
||||||
sess["rooms"] = set()
|
sess["rooms"] = set()
|
||||||
sess["nick"] = None
|
sess["nick"] = None
|
||||||
|
sess["peer_caps"] = {}
|
||||||
|
|
||||||
# Process the HELLO message
|
# Process the HELLO message
|
||||||
if isinstance(body, dict):
|
if isinstance(nick, str):
|
||||||
nick = body.get(B_HELLO_NICK)
|
|
||||||
n = normalize_nick(nick, max_chars=self.config.nick_max_chars)
|
n = normalize_nick(nick, max_chars=self.config.nick_max_chars)
|
||||||
if n is not None:
|
if n is not None:
|
||||||
sess["nick"] = n
|
sess["nick"] = n
|
||||||
|
|
||||||
|
if isinstance(body, dict):
|
||||||
|
sess["peer_caps"] = self._extract_caps(body)
|
||||||
|
if sess.get("nick") is None:
|
||||||
|
legacy_nick = body.get(B_HELLO_NICK_LEGACY)
|
||||||
|
n2 = normalize_nick(legacy_nick, max_chars=self.config.nick_max_chars)
|
||||||
|
if n2 is not None:
|
||||||
|
sess["nick"] = n2
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"Re-HELLO peer=%s nick=%r link_id=%s",
|
"Re-HELLO peer=%s nick=%r link_id=%s",
|
||||||
self._fmt_hash(peer_hash),
|
self._fmt_hash(peer_hash),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from rrcd.constants import (
|
from rrcd.constants import (
|
||||||
B_HELLO_NICK,
|
B_HELLO_NICK_LEGACY,
|
||||||
K_BODY,
|
K_BODY,
|
||||||
K_ID,
|
K_ID,
|
||||||
K_NICK,
|
K_NICK,
|
||||||
@@ -16,7 +16,7 @@ from rrcd.envelope import make_envelope, validate_envelope
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_accepts_make_envelope() -> None:
|
def test_validate_accepts_make_envelope() -> None:
|
||||||
env = make_envelope(T_HELLO, src=b"peer", body={B_HELLO_NICK: "alice"})
|
env = make_envelope(T_HELLO, src=b"peer", body={B_HELLO_NICK_LEGACY: "alice"})
|
||||||
validate_envelope(env)
|
validate_envelope(env)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user