diff options
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 45 |
1 files changed, 44 insertions, 1 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index a511093..6b1fd05 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -1588,6 +1588,10 @@ everyone in it (2026-09-11). The ceiling applies to **every** message that chang set, not only to the registration: a node that may narrow on connecting and widen on reload has no ceiling. +The socket is accepted before anyone is known, so **the auth message must arrive +within ten seconds** or the socket is closed with 4001: an unbounded first read is a +connection any stranger holds open for free. The node sends it on connecting. + A client must therefore treat that list as candidates rather than a ranking, and try the next node on a `not_hosted` refusal (`MESHBAY_NODE_PROTOCOL.md` §6.3). @@ -1659,6 +1663,20 @@ keep their membership and their access. A node whose operator set `join_policy = "open"` still pins and serves whoever reaches it directly over MNP; what the switch removes is the hub-provided ways to find and reach such a node. +**What the hub answers without an account is a reviewed list.** +`test_unauthenticated_surface.py` walks every route and fails on one that takes no +authentication dependency and is not listed there with its reason; routes that +authenticate in their own body (a signature, an e-mailed code, an MHP token) are +listed with what they check. The hub publishes no API description — no `/docs`, +`/redoc` or `/openapi.json` — in the code, not in a proxy rule, so a packaged +install behind any proxy publishes none either. + +The mail bounds (`mail.*`) and the sign-in lockout (`login.max_failures`, +default 4, and `login.lockout_minutes`, default 60 — §7.7) live in the same table +for the same reason: they are what an operator changes while the hub is serving, +from the panel, without a restart. Each value is clamped to published bounds, and +`max_failures = 0` turns the lockout off. + ### 7.5 Moderation Two verbs on a group, and they are distinct things: @@ -1760,6 +1778,30 @@ Registration is gated by a CAPTCHA whenever one is configured — **unconditiona not only when some other field is absent, or the real client's ordinary request skips it. The desktop client renders the widget too. +**Passphrase sign-in locks per username.** After `login.max_failures` wrong +passphrases (§7.4) the name is refused with `429 account_locked` and a +`Retry-After` for `login.lockout_minutes`, without the passphrase being checked. +The per-IP rate limit bounds one address, and IPv6 hands every subscriber a /64 +of them; an online guess targets an account, so the account is what is counted. +The rules that make this safe: + +- **Counted by the name as typed, existing or not.** An unknown name locks exactly + like a real one, so `login` stays uniform (**M1**). The key is a hash: people + type passphrases into the username field. +- **The attempt is taken before the check, in one statement** — an `INSERT … ON + CONFLICT DO UPDATE … WHERE … RETURNING` — so a concurrent burst gets no more + attempts than the limit. A request that checked no passphrase gives its attempt + back. +- **Every path that checks the passphrase counts on the same row**: sign-in, + passphrase change and account deletion. A right passphrase clears it; failures + older than the window age out. +- **A lockout refuses passphrase sign-in and nothing else.** Open sessions, token + renewal and device sign-in continue, and a reset code sent to the address on + file clears it — so a stranger who locks a public username costs its owner at + most a new sign-in (**AV26**). A session learns its own lockout from + `/v1/users/me`, because a passphrase change re-wraps every node's bundle before + the hub accepts the new passphrase and must not start when the hub would refuse. + --- ## 8. Clients @@ -2641,6 +2683,7 @@ had already been asked. | **AV23** | **An upload's owner is recorded when the upload ends and applied when the entry is created**, which are different moments (§5.4). Written against the index at the end of the upload it matched nothing, every time, and left every uploaded file owned by nobody — so no member could delete what they had sent | | **AV24** | **A node registered for no group is refused signaling, not exempted from it** (§7.2). The membership check was written as "if the node claims any group", so it skipped itself — membership, group status and the public-group gate together — for the node AV1 made commonplace: the unconfigured one, which is also the one least able to absorb the work | | **AV25** | **Which nodes host a group is answered to its members** (§7.3). Only the public case checked, so a private group told any authenticated account that knew its id which machines hosted it — and an ex-member knows that id for ever | +| **AV26** | **A sign-in lockout refuses passphrase sign-in and nothing else** (§7.7). It is keyed by username, usernames are public, and so anyone can spend somebody else's attempts. Open sessions, renewal and device sign-in are untouched and a reset code ends it, which bounds what a stranger buys to one forced sign-in. The lockout is a DoS primitive by construction; this is the ceiling on it | ### 13.6 Chat design findings @@ -2796,7 +2839,7 @@ account recovery, and the Windows port through packaging. | The exact-hash content check | Structural, not functional (§7.5) | | **The packaged install** | Built and never installed. `build-packages.sh` produces four `.deb` that carry the migrations and a relocatable entry point, and no machine has been taken from those packages to a running hub and node. Every packaging defect found so far was found the first time somebody tried | | **QUIC** | Off by default, and **not at parity**: it serves the index and file chunks with no transfer lease, no leaseless ceiling and no root-availability check, does its file I/O on the event loop, and returns exception text to the peer (**L3**). No client speaks it. Either it comes to parity or it goes; until then §5.1's "chat is the only gap" is the one sentence here that overstates the code | -| **The relay registry** | `/v1/relays` register/list/approve exist and nothing in the tree calls them, node or client — and §11.1 measured two ISPs with no TURN relay needed. Kept code that nothing calls is what **L7** says not to keep | +| **The relay registry** | **Closed in the code**: `relay.RELAYS_ENABLED` is False and every `/v1/relays` route answers 503, as federation does. Nothing in the tree calls them, node or client, and §11.1 measured two ISPs with no TURN relay needed. Kept code that nothing calls is what **L7** says not to keep; it stays only as the proof-of-possession design (**AV6**) until a node needs a relay or it is deleted | | **Free-text third-party search** | `tmdb_search_req` takes a member's query and spends the operator's per-credential quota with no rate limit and no per-member bound, where link previews carry both. §6.5's standing rule — a bound and a named adversary in the same commit — was not applied here | | **Disk I/O on the node's event loop** | A chunk read-and-encrypt and every upload chunk write run in the message handler. On a spun-down or network-mounted root that stalls every group, every stream and the hub socket, which is `AV9`'s lesson with the disk in place of the mail server | | **Node announcements are not bounded** | One account may announce unlimited distinct node keys, each a row plus an IP-log row under a one-year retention. Proof of possession is checked (**M8**); the count is not | |