aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md92
-rw-r--r--docs/QUICKSTART.md10
-rw-r--r--docs/USERGUIDE.md27
3 files changed, 116 insertions, 13 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 4e999949..1480cd57 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -1486,9 +1486,12 @@ download land on different processes without the client doing anything.
| 8 | 150 MB/s, the receiving browser being the limit | 21 % |
Measured on loopback, a 1.5 GB file through the interface in a headless browser,
-byte for byte. A process costs about 85 MB of memory. The default is 1, which
-starts none and leaves the node exactly as it was; the operator raises it. A
-change applies to new connections, and a connection already open stays where it
+byte for byte. A process costs about 85 MB of memory. The default is one per
+core, up to four: past four a single download gains nothing, and a process is
+memory held for the life of the node. One starts none, and leaves every
+connection, every lane of a download included, on the node's own core — the
+case where Python's per-packet cost is the whole ceiling, and highest on Windows.
+A change applies to new connections, and a connection already open stays where it
is until it closes, in a process asked to leave or in the node. A process that
dies takes its own connections with it, which their clients see as a dropped
connection and reopen, and is replaced at the next offer. The loopback socket is
@@ -1512,6 +1515,61 @@ machine nothing changes until the operator raises it; the node says so at startu
The limit that remains is the node's own receive path, about 5 % of a core per
MB/s.
+**A receiver on aiortc reassembles in linear time and acknowledges every second
+packet.** That is the command-line client in a download and the node in an
+upload. aiortc 1.15 walks the reassembly from its start after every DATA chunk,
+so a message of n fragments costs n²/2 steps — a 1 MiB `file_chunk` is about 870
+— and it acknowledges every packet, where RFC 4960 §6.2 asks for one SACK every
+second packet; each SACK costs the sender a DTLS decryption and a parse on the
+core that also sends. `sctp_fix.py` (the node's, and the client's copy) skips the
+walk until a last fragment is in, and acknowledges every second packet of data,
+at once on a gap, a duplicate or a packet carrying anything else, and within
+200 ms otherwise. The client asks for a 4 MB receive buffer, as the node does.
+Measured on loopback, 400 MB to the command-line client over one connection: 19
+MB/s with 3245 datagrams dropped by its kernel; none dropped with the buffer, and
+still 20, the client at a full core; 28 with the reassembly fixed; 39 with the
+acknowledgements, 49 by the node's own count of the transfer. An upload from the
+client went from 25 to 29.
+
+**On Windows a datagram goes straight through the socket, in both directions.**
+The node and its connection processes run on the ProactorEventLoop, the one that
+can start ffmpeg there, and its datagram transport keeps one overlapped operation
+per direction, starting the next from the completion callback of the one before,
+on a later turn of the loop. A socket therefore sent one datagram per turn and
+received one, and a turn lasts as long as everything else the loop does: the
+other connections, the file, the hub. That is a ceiling per connection that
+falls as the node gets busier, with cores to spare. With the loop otherwise busy
+0.5 ms a turn: 1,928 datagrams sent a second, and 1,954 received with 87 % of
+what arrived dropped by the kernel. In a node holding its connections itself, a
+browser's download over eight of them ran at 9 MB/s with the node at 45 % of a
+core and an SCTP round trip of 745 ms on loopback, and one of the eight sessions
+was lost on the way. `proactor_udp.py` sends with a non-blocking `sendto` when
+nothing is queued, and after a completed read takes whatever else the socket
+holds before posting the next one; refusals and errors take the proactor's own
+path. Measured on loopback on a two-core machine, 400 MB, with it: the
+command-line client's download 27 to 33 MB/s, its upload 18 to 22 (23 with two
+connection processes), the browser's download over eight connections 9 to 14
+before and 17 to 19 after, where the browser takes the other core.
+
+**A session is open from the end of its handshake, and a reconnect does not
+shorten that.** Until connect() returns, a client sends only what the node takes
+before the proof — `handshake`, `handshake_response`, `keypair_bundle_fetch`,
+`gek_bundle_fetch`, `join_request`, `device_add_request` — and `device_hello`;
+anything else waits for the session, or fails as a dropped connection, which a
+download waits out. The node answers anything else with "Handshake required", a
+refusal and not a dropped connection, so a chunk let through while a reconnect's
+handshake runs ends the download the reconnect is for. The close event of a
+channel a reconnect has replaced concerns nothing on the new one.
+
+**A connection follows the device to another network.** A phone moving between
+mobile data and the Wi-Fi the node is on keeps a candidate pair over the
+network it left, and ICE says so only once its consent checks lapse, about
+thirty seconds. So a change of network (`online`, the connection's `type`, the
+Android application's `meshbay-default-network`) and an ICE `disconnected` each
+ask the session for a ping, and a session that does not answer within five
+seconds is reconnected; a reconnect backing off tries again at once; a group
+page left on an error tries again (`transport.js` `onNetworkChange`).
+
The lease module is free of asyncio and of the transport: it decides, and the
caller does the I/O. A queue that reveals itself only through a DataChannel is a
queue nobody can prove things about.
@@ -2163,7 +2221,7 @@ Node page:
| `max_concurrent_downloads` | 8 | node-wide download leases (§5.5) |
| `max_concurrent_uploads` | 8 | node-wide upload leases. A separate pool from downloads, because the two cost different things and one queue for both makes each cap meaningless |
| `max_upload_gb` | 8 | the largest single upload, §6.4. Fractions are allowed, and it is read per chunk so a change reaches an upload already running |
-| `connection_processes` | 1 | how many processes carry the peer connections, §5.5. One is the node itself; above it, that many cores serve transfers. Bounded by the cores of the machine, which the Node page shows beside the field |
+| `connection_processes` | one per core, up to 4 | how many processes carry the peer connections, §5.5. One is the node itself; above it, that many cores serve transfers. Bounded by the cores of the machine, which the Node page shows beside the field. `node.toml` as `init` writes it leaves the line commented, so the default reaches every node that has not chosen |
| `transcode_incompatible_video` | true | whether browser-incompatible video is transcoded during streaming. Unlike remuxing this costs real CPU per viewer |
`hardware_video_encode` (default true) governs whether that transcode may use the
@@ -4110,6 +4168,30 @@ narrow bridge — and Android has all three:
notification's link is a route in the page, applied as `location.hash`, never
loaded. No VAPID yet: a distributor that requires it refuses, and the phone
fetches instead.
+- **The local network is a permission the person grants** (Android 17,
+ `ACCESS_LOCAL_NETWORK`, shown as "Nearby devices"), since the application
+ targets SDK 37. The node is usually on the phone's Wi-Fi and the WebView's
+ WebRTC carries the application's permission: without it the node's
+ addresses on that Wi-Fi are dropped, and the group opens on mobile data and
+ not at home. `shell/LocalNetwork.kt` asks when a group is first opened,
+ never at start; a refusal the connection runs into is named
+ (`local_network_blocked`) and the group page offers the question again
+ while Android will still ask, then the application's settings. Moving to
+ SDK 37 also took the permission back from an installed application, which
+ had it implied before: the first group opened after the update asks.
+ A phone connected on mobile data that walks onto the node's Wi-Fi without
+ the permission, in the application or in Chrome, loses its connection and
+ is refused on every reconnect attempt: each refusal reaches the group page
+ (`addReconnectFailureListener`), which says the same as at opening over the
+ view it had, while the reconnect goes on. The page's buttons ask that
+ reconnect to try now rather than tear it down, so a download or a film
+ under way resumes once the permission is granted or mobile data is back;
+ an attempt still waiting on ICE is given up for a new one, since Chrome
+ keeps refusing what it dropped when that attempt began.
+- **The page hears when the phone changes network** (`meshbay-default-network`,
+ from the default network callback), mobile data to Wi-Fi or back, and checks
+ the connection it holds (transport.js `onNetworkChange`). The event carries
+ nothing about either network.
Release builds are signed with the release key, distributed as a direct APK;
the key stays outside the repository and a release build without it fails
@@ -4732,7 +4814,6 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| Metadata at the hub | Membership, and who posted in which group and when. A known leak, not a solved problem (§7.1) |
| **QUIC** | Off by default, and **not at parity**: it serves the index and file chunks with no transfer lease, no leaseless ceiling, no root-availability check and no content blocklist, 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 |
| **A very high bitrate wedges the player against a small buffer ceiling** | Where even the *floor* read-ahead does not fit — ninety seconds plus the minute kept behind, at the file's bitrate, above what the engine will hold — the film stalls: measured on the harness at 9.3 Mbit/s against a 100 MB ceiling, 100.8 s of film played in 900 s of wall clock. **Predates the byte budget and is unchanged by it**, to the tenth of a second; what the budget did change there is the refusal count, 1560 → 2. The fix is not a bound at all, it is a second stage of buffer outside the SourceBuffer, which means gating the append path — the riskiest change in this area and not one to make alongside another |
-| **The reconnect backoff only wakes on `visibilitychange`** | So a tab that stays visible through an outage — which is what a screen wake lock guarantees while a film is playing — waits out the full backoff, up to 30 s, after the network is already back. Nothing listens for `online` |
| **Per-device revocation has no CLI** | A device is revoked over MNP (`roster.revoke_device`), from a device the node has already pinned. On a headless node the operator's only lever is `member unpin`, which removes **every** device of that account — so the per-device control the roster is built around is reachable from an interface and from nowhere else. §6.7 listed a `meshbay-node member device list\|revoke` verb that was never written, and that listing is how this was found: `USERGUIDE.md` was the first document written by reading the CLI rather than this specification, and the verb it copied out did not run |
| **Migrations run on SQLite only** | The chain reaches head and agrees with the models there (§12), which is not where it ships. **The exposure is one revision deep, not the whole chain**: every revision behind the first packaged release was development that no installation ever ran, so nothing replays them on PostgreSQL. What is unguarded is the *next* migration — a default, an index type or a constraint PostgreSQL refuses reaches a deploy without the suite saying so |
| **The Create group wizard calls two hooks after an early return** | `CreateGroupWizard` (`create-group-page.js`) returns during node detection, before its `useRef`/`useEffect` for provisioning, so the hook count changes between renders. Preact tolerates a list that grows, and nothing is known to break; `test_hook_ordering.py` checks declaration order, not this. Found while tracing the frozen-fields report, which had another cause (`ask.js`) |
@@ -4746,6 +4827,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| **A failed download leaves its in-flight chunks rejecting unhandled** | When `pipelinedDownload` throws, the other promises of its window are abandoned and each prints "Uncaught (in promise)" in the console. Noise, but in the log a dropped connection is read from |
| **A queued transfer logs "no answer" every minute** | The lease watchdog re-asks after 60 s of silence whatever the last answer was, so a lease the node has answered `queued` and that has not moved prints "no answer for transfer ... asking again" once a minute for as long as it waits, which reads as a fault |
| **"Prune stale entries" does not touch the index** | The Node page's maintenance button runs `prune_index_cache`, which drops rows of the hash cache and nothing else. An operator who sees a deleted file still listed reaches for it, and nothing changes: only the reconcile removes an index entry, and nothing lets the operator ask for one |
+| **Only the group page explains a refused local network** | `transport.js` names it (`local_network_blocked`, Android 17 and Chrome 155), and `group-page.js` says what to change. Search and the other screens that open a connection show it as a node that cannot be reached |
---
diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md
index dc263b8d..9f1040e0 100644
--- a/docs/QUICKSTART.md
+++ b/docs/QUICKSTART.md
@@ -376,7 +376,7 @@ config /home/you/.config/meshbay/node.toml
operator yourname key AAAC3NzaC1lZDI1… paired 2026-09-18T14:02:11Z
```
-If a line is missing, `status` names the command that fixes it. The four that
+If a line is missing, `status` names the command that fixes it. The ones that
catch most people:
| Symptom | Cause |
@@ -385,6 +385,7 @@ catch most people:
| `operator NONE PAIRED` | Step 8 — invites and file deletion are refused until this is done |
| an `action needed:` block naming a group key | Step 7's `gek init` was skipped — `meshbay-node group list` shows it as `NO KEY` |
| Group shows files to you, nothing to a member | The member never redeemed a code, or the firewall in Step 9 |
+| An Android phone opens the group on mobile data, not on your Wi-Fi | Android 17 lets a browser reach your local network only once allowed: **Settings → Apps → Chrome → Permissions → Nearby devices → Allow** ([`USERGUIDE.md` §9](USERGUIDE.md#9-when-something-breaks)) |
---
@@ -397,7 +398,12 @@ catch most people:
Files and Settings always stay.
- **Add your other devices.** Your phone's browser, the desktop application:
each one asks to be recognised, and you approve it from a device already
- linked. Nobody else is involved.
+ linked. Nobody else is involved. On an Android phone at home, allow the
+ browser to reach your local network the first time (**Settings → Apps →
+ Chrome → Permissions → Nearby devices**): Android 17 requires it and
+ Chrome does not ask, and without it the phone reaches your node only on
+ mobile data. The MeshBay Android application asks for it when you first
+ open a group: answer **Allow**.
- **Read [`USERGUIDE.md`](USERGUIDE.md)** — what each application does, how
members are managed, what happens when someone leaves, what is private and
what is not, and what to do when something breaks.
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index 18085982..79481106 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -852,15 +852,16 @@ and running `meshbay-node reload`:
| `max_concurrent_downloads` | 8 | node-wide download slots |
| `max_concurrent_uploads` | 8 | node-wide upload slots |
| `max_upload_gb` | 8 | the largest single file a member may send you |
-| `connection_processes` | 1 | how many CPU cores carry members' connections |
+| `connection_processes` | one per core, up to 4 | how many CPU cores carry members' connections |
| `transcode_incompatible_video` | true | re-encode video no browser can play |
| `hardware_video_encode` | true | use the GPU for that, if one actually works |
-`connection_processes` is the one for speed. With 1, everything a node sends
-goes through one core, about 35 MB/s shared between everyone downloading at
-that moment. Raise it when several people download at once and the machine has
-cores to spare: each one adds a core's worth, and about 85 MB of memory. It
-applies to connections opened after the change.
+`connection_processes` is the one for speed. Each process is a core's worth
+of sending, shared between everyone downloading at that moment, and about 85 MB
+of memory. With 1, everything a node sends goes through one core: about 35 MB/s
+on Linux, much less on Windows. Lower it on a machine short of memory; raise it
+above 4 only when many people download at once. It applies to connections
+opened after the change.
`max_concurrent_streams` is the one to think about: a stream costs a video
process for as long as the film lasts, so this counts **viewers**, not
@@ -1102,6 +1103,20 @@ The firewall. A browser on your LAN has to be able to call the node, and a node
refusing unsolicited inbound UDP cannot be called. See
[`QUICKSTART.md` Step 9](QUICKSTART.md#step-9--let-people-reach-you).
+**On an Android phone, a group opens on mobile data but not on the Wi-Fi its node is on.**
+Android 17 lets an application reach devices on the local network only once
+you allow it, and Chrome comes under that rule from version 155 without asking
+you. Until you allow it, Chrome on the phone cannot reach a node on its own
+Wi-Fi, while on mobile data it reaches the node from outside, so the group
+opens. The group says so rather than
+"DataChannel closed". To allow it: Android **Settings → Apps → Chrome →
+Permissions → Nearby devices → Allow**, then **Retry**. The same goes for any
+other browser you use on the phone. A browser on a computer is not affected.
+The MeshBay Android application asks for it itself the first time you open a
+group. If you said no, the group offers **Allow** to be asked again, or, once
+Android stops asking, **Open settings**: there, **Permissions → Nearby
+devices → Allow**, then come back to the application.
+
**A shared directory reads as empty and everything else looks right.**
A mount the service cannot see. The systemd drop-in needs
`RequiresMountsFor` as well as `ReadWritePaths` — the service has its own mount