From 69554fac7eba6eef7eb8a1c0111c5b92e7f21256 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 11:22:24 +0200 Subject: fix: a member can no longer lock a node, crash it with a link, or stop hub cleanup - node: only a wrong code counts towards the join lock, now per account (5) as well as node-wide (20), and it is consulted only when a code is tried. Every member reconnecting gets the group key through join_request, so a lock checked before recognition let one member refuse it to everyone. - node: link previews read the body as a stream and stop at the cap, counted on decoded bytes; a declared oversized image is not read; 15 s total deadline; image decoding off the loop. `client.get` had buffered the whole (decompressed) response before the caps looked at it. - hub: the daily purge of never-verified accounts detaches their IP-log rows (keeping the name) and clears every other reference first, and each cleanup step runs on its own. On PostgreSQL the bare DELETE violated the ip_logs foreign key and stopped every purge behind it for good. Co-Authored-By: Claude Opus 5.5 --- docs/MESHBAY_NODE_PROTOCOL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs/MESHBAY_NODE_PROTOCOL.md') diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index ce99a40..2e5aca3 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -620,7 +620,7 @@ immediately after these three, so the table below is the window, exhaustively. |---|---|---| | `keypair_bundle_fetch` | the client's own identity keys for this node live in an encrypted bundle stored on it | counts against `MAX_PRE_PROOF_FETCHES` = 4; audited | | `gek_bundle_fetch` | the wrapped group key is what the proof is computed with | same counter | -| `join_request` | a first-time member holds no group key at all. Accepted after the proof as well — an operator pairing a browser is already connected — because its authority comes from the pairing code and the signature, never from the session state | 5 attempts per connection, 20 failures per 600 s node-wide | +| `join_request` | a first-time member holds no group key at all. Accepted after the proof as well — an operator pairing a browser is already connected — because its authority comes from the pairing code and the signature, never from the session state | 5 attempts per connection; wrong codes: 5 per account and 20 node-wide per 600 s, consulted only when a code is tried | Device linking (§9) is **not** in this window. `device_add_request` and every message after it are answered only on an authenticated session, and the device budget of 5 @@ -822,7 +822,6 @@ Evaluated in order (`_do_join_request`): | Condition | Outcome | |---|---| | `join_attempts >= 5` on this connection | `error: Too many attempts` | -| `>= 20` node-wide failures in 600 s | `error: Pairing temporarily locked`, audited `join_throttled` | | key not 32 raw bytes, or bad base64 | `join_result{ok:false, reason:"invalid_keys"}` | | `\|ts - now\| > 120` | `stale_request` | | `group_id` non-empty and != session group | `group_mismatch` | @@ -831,6 +830,7 @@ Evaluated in order (`_do_join_request`): | account has devices here, this key is not one | `unknown_device` — the way in is a device-add (§9), not a new invite | | device known, no member row, group policy `open` | member row created (`approved_by: "open-join"`) | | device known, a **pending invite** exists for this user | code required even for a known device; `code_required` / `code_invalid` on failure | +| a code is about to be tried and this account has `>= 5`, or the node `>= 20`, wrong codes in 600 s | `error: Pairing temporarily locked`, audited `join_throttled`. Only `code_invalid` counts, and nothing else is gated by it: every member reconnecting gets the key through this message, so a lock checked before recognition let one member refuse it to everybody | | device known, not an active member of the session group, a code offered | redeemed like any code — an invitation **link** reaches here from someone pinned through another group, or removed and invited back; `code_invalid` on failure | | device known, member row resolved | `join_result{ok, recognised:true, role}` + wrapped GEK | | unknown device, no code, policy `open` | pin TOFU, admit, wrap (`via: "tofu"`, audited) | @@ -2366,7 +2366,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions | Code lifetimes (default, settable) | invitation 7 d, operator pairing 24 h, device request 1 h | `roster.py` | | `MAX_PRE_PROOF_FETCHES` | 4 per connection | `webrtc/dispatch.py` | | `MAX_JOIN_ATTEMPTS` | 5 per connection | `webrtc/admission.py` | -| `MAX_JOIN_FAILURES_WINDOW` / `JOIN_FAILURE_WINDOW` | 20 / 600 s, node-wide | ” | +| `MAX_JOIN_FAILURES_PER_ACCOUNT` / `MAX_JOIN_FAILURES_WINDOW` / `JOIN_FAILURE_WINDOW` | 5 per account / 20 node-wide / 600 s, wrong codes only | ” | | Device attempts | 5 per connection | ” | | `MAX_DEVICES_PER_USER` | 5 | `roster.py` | | `MAX_LINK_INVITES_PER_GROUP` | 20 unredeemed invitation links | `roster.py` | -- cgit v1.2.3