update nickname and room name limits to use byte size instead of character count

This commit is contained in:
kc1awv
2026-01-15 15:43:35 -05:00
parent 49b0702334
commit 02edfd7e38
7 changed files with 61 additions and 40 deletions
+17 -9
View File
@@ -146,10 +146,6 @@ room_invite_timeout_s = 900.0
# Optional behaviors.
include_joined_member_list = false
# Nickname policy.
# Maximum accepted nickname length (Unicode characters). 0 disables length limiting.
nick_max_chars = 32
# Limits.
# These limits help mitigate abuse and resource exhaustion, but can be adjusted
# based on your use case.
@@ -157,9 +153,10 @@ nick_max_chars = 32
# N.B. max_msg_body_bytes should not allow messages so large that they cannot
# fit within the link MTU after UTF-8 encoding and envelope overhead. The
# default of 350 bytes is a safe choice for the default Reticulum MTU of 500.
max_rooms_per_session = 32
max_room_name_len = 64
max_nick_bytes = 32
max_room_name_bytes = 64
max_msg_body_bytes = 350
max_rooms_per_session = 32
rate_limit_msgs_per_minute = 240
# Hub-initiated liveness checks (0 disables).
@@ -330,7 +327,16 @@ def _build_arg_parser() -> argparse.ArgumentParser:
p.add_argument("--max-rooms", type=int, default=None, help="Max rooms per session")
p.add_argument(
"--max-room-name-len", type=int, default=None, help="Max room name length"
"--max-nick-bytes",
type=int,
default=None,
help="Max nickname size in UTF-8 bytes",
)
p.add_argument(
"--max-room-name-bytes",
type=int,
default=None,
help="Max room name size in UTF-8 bytes",
)
p.add_argument(
@@ -426,8 +432,10 @@ def main(argv: list[str] | None = None) -> None:
if args.max_rooms is not None:
cfg = replace(cfg, max_rooms_per_session=int(args.max_rooms))
if args.max_room_name_len is not None:
cfg = replace(cfg, max_room_name_len=int(args.max_room_name_len))
if args.max_nick_bytes is not None:
cfg = replace(cfg, max_nick_bytes=int(args.max_nick_bytes))
if args.max_room_name_bytes is not None:
cfg = replace(cfg, max_room_name_bytes=int(args.max_room_name_bytes))
if args.rate_limit_msgs_per_minute is not None:
cfg = replace(
+2 -2
View File
@@ -25,9 +25,9 @@ class HubRuntimeConfig:
room_registry_prune_interval_s: float = 3600.0
room_invite_timeout_s: float = 900.0
include_joined_member_list: bool = False
nick_max_chars: int = 32
max_nick_bytes: int = 32
max_rooms_per_session: int = 32
max_room_name_len: int = 64
max_room_name_bytes: int = 64
max_msg_body_bytes: int = 350
rate_limit_msgs_per_minute: int = 240
ping_interval_s: float = 0.0
+8 -8
View File
@@ -300,7 +300,7 @@ class MessageRouter:
new_nick = None
if isinstance(nick, str):
n = normalize_nick(nick, max_chars=self.hub.config.nick_max_chars)
n = normalize_nick(nick, max_bytes=self.hub.config.max_nick_bytes)
if n is not None:
new_nick = n
sess["nick"] = n
@@ -311,7 +311,7 @@ class MessageRouter:
if new_nick is None:
legacy_nick = body.get(B_HELLO_NICK_LEGACY)
n2 = normalize_nick(
legacy_nick, max_chars=self.hub.config.nick_max_chars
legacy_nick, max_bytes=self.hub.config.max_nick_bytes
)
if n2 is not None:
new_nick = n2
@@ -360,7 +360,7 @@ class MessageRouter:
new_nick = None
if isinstance(nick, str):
n = normalize_nick(nick, max_chars=self.hub.config.nick_max_chars)
n = normalize_nick(nick, max_bytes=self.hub.config.max_nick_bytes)
if n is not None:
new_nick = n
sess["nick"] = n
@@ -370,7 +370,7 @@ class MessageRouter:
if new_nick is None:
legacy_nick = body.get(B_HELLO_NICK_LEGACY)
n2 = normalize_nick(
legacy_nick, max_chars=self.hub.config.nick_max_chars
legacy_nick, max_bytes=self.hub.config.max_nick_bytes
)
if n2 is not None:
new_nick = n2
@@ -686,10 +686,10 @@ class MessageRouter:
text="message requires room name",
)
return
# Validate message body size (UTF-8 bytes)
if isinstance(body, str):
body_bytes = len(body.encode('utf-8', errors='replace'))
body_bytes = len(body.encode("utf-8", errors="replace"))
if body_bytes > self.hub.config.max_msg_body_bytes:
if self.hub.identity is not None:
self.hub.message_helper.emit_error(
@@ -781,7 +781,7 @@ class MessageRouter:
incoming_nick = env.get(K_NICK)
if incoming_nick is not None:
n = normalize_nick(incoming_nick, max_chars=self.hub.config.nick_max_chars)
n = normalize_nick(incoming_nick, max_bytes=self.hub.config.max_nick_bytes)
if n is not None:
old_session_nick = sess.get("nick")
if old_session_nick != n:
@@ -794,7 +794,7 @@ class MessageRouter:
env.pop(K_NICK, None)
else:
nick = sess.get("nick")
n = normalize_nick(nick, max_chars=self.hub.config.nick_max_chars)
n = normalize_nick(nick, max_bytes=self.hub.config.max_nick_bytes)
if n is not None:
env[K_NICK] = n
+10 -6
View File
@@ -97,10 +97,10 @@ class HubService:
self.destination.hash.hex() if self.destination else "-",
)
self.log.info(
"Policy nick_max_chars=%s max_rooms=%s max_room_name_len=%s rate_limit_msgs_per_minute=%s",
self.config.nick_max_chars,
"Policy max_nick_bytes=%s max_rooms=%s max_room_name_bytes=%s rate_limit_msgs_per_minute=%s",
self.config.max_nick_bytes,
self.config.max_rooms_per_session,
self.config.max_room_name_len,
self.config.max_room_name_bytes,
self.config.rate_limit_msgs_per_minute,
)
@@ -330,7 +330,7 @@ class HubService:
f"banned={len(old_banned)}->{len(new_banned)} "
f"registered_rooms={len(old_registry)}->{len(new_registry)}"
)
lines.append(f"policy: nick_max_chars={new_cfg.nick_max_chars}")
lines.append(f"policy: max_nick_bytes={new_cfg.max_nick_bytes}")
if cfg_changes:
lines.append("config_changes:")
@@ -503,8 +503,12 @@ class HubService:
r = room.strip().lower()
if not r:
raise ValueError("room name must not be empty")
if len(r) > int(self.config.max_room_name_len):
raise ValueError("room name too long")
# Check UTF-8 byte length
room_bytes = len(r.encode("utf-8", errors="replace"))
if room_bytes > int(self.config.max_room_name_bytes):
raise ValueError(
f"room name too long: {room_bytes} bytes > {self.config.max_room_name_bytes} bytes"
)
return r
def _on_packet(self, link: RNS.Link, data: bytes) -> None:
+11 -3
View File
@@ -135,16 +135,24 @@ class SessionManager:
self.hub.room_manager.remove_member(room, link)
if remaining_members and peer_hash and self.hub.identity:
notification_body = [peer_hash] if self.hub.config.include_joined_member_list else None
notification_body = (
[peer_hash] if self.hub.config.include_joined_member_list else None
)
member_notification = make_envelope(
T_PARTED, src=self.hub.identity.hash, room=room, body=notification_body
T_PARTED,
src=self.hub.identity.hash,
room=room,
body=notification_body,
)
member_notification_payload = encode(member_notification)
for member_link in remaining_members:
try:
import RNS
RNS.Packet(member_link, member_notification_payload).send()
self.hub.stats_manager.inc("bytes_out", len(member_notification_payload))
self.hub.stats_manager.inc(
"bytes_out", len(member_notification_payload)
)
except Exception:
pass
+2 -2
View File
@@ -109,8 +109,8 @@ class StatsManager:
lines.append(
f"limits: rate_limit_msgs_per_minute={self.hub.config.rate_limit_msgs_per_minute} "
f"max_rooms_per_session={self.hub.config.max_rooms_per_session} "
f"max_room_name_len={self.hub.config.max_room_name_len} "
f"nick_max_chars={self.hub.config.nick_max_chars}"
f"max_room_name_bytes={self.hub.config.max_room_name_bytes} "
f"max_nick_bytes={self.hub.config.max_nick_bytes}"
)
lines.append(
f"features: ping_interval_s={self.hub.config.ping_interval_s} "
+11 -10
View File
@@ -2,14 +2,14 @@ from __future__ import annotations
import os
_DEFAULT_NICK_MAX_CHARS = 32
_DEFAULT_NICK_MAX_BYTES = 32
def expand_path(p: str) -> str:
return os.path.expanduser(os.path.expandvars(p))
def normalize_nick(value, *, max_chars: int = _DEFAULT_NICK_MAX_CHARS) -> str | None:
def normalize_nick(value, *, max_bytes: int = _DEFAULT_NICK_MAX_BYTES) -> str | None:
if not isinstance(value, str):
return None
@@ -18,19 +18,20 @@ def normalize_nick(value, *, max_chars: int = _DEFAULT_NICK_MAX_CHARS) -> str |
return None
try:
limit = int(max_chars)
limit = int(max_bytes)
except Exception:
limit = int(_DEFAULT_NICK_MAX_CHARS)
limit = int(_DEFAULT_NICK_MAX_BYTES)
if limit > 0 and len(s) > limit:
# Check UTF-8 byte length
try:
encoded = s.encode("utf-8", "strict")
except UnicodeError:
return None
if limit > 0 and len(encoded) > limit:
return None
if "\n" in s or "\r" in s or "\x00" in s:
return None
try:
s.encode("utf-8", "strict")
except UnicodeError:
return None
return s