forked from retoor/devplacepy
Phase 3 of plan.md. Device registration with one-time secrets, scoped single-use socket tickets, the lock-owner WS /nodes/ws with handshake and heartbeat eviction, a single batch ConsentGate, and NodesService which disconnects nodes within one tick when consent, device or the nodes_enabled kill switch changes. A reconcile of 2000 sessions measured about 110 ms after replacing per-session queries with batches. Devii may withdraw but never grant bandwidth_sharing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
83 lines
4.5 KiB
HTML
83 lines
4.5 KiB
HTML
<div class="docs-content" data-render>
|
|
# Node network protocol
|
|
|
|
This page is generated from the protocol specification, `services/nodes/spec.py`, the same data served at [`/nodes/spec.json`](/nodes/spec.json). The server, the clients, the tests and this page all read that one source, so the numbers below cannot drift from the code.
|
|
|
|
**Status today:** the specification, `/nodes/spec.json`, device registration, the socket ticket, the node socket and the consent gate are implemented on the server and are switched off by default. The clients and the search integration are not built yet, so no node can do anything useful and no job is ever sent. Nothing on this page is enabled for any account.
|
|
|
|
## Rules that never change
|
|
|
|
- **Not an open proxy.** `open_proxy` is `{{ nodes_spec.open_proxy|lower }}`. A node only ever contacts hosts on the allowlist below. Changing that is a protocol change, not a setting.
|
|
- **Web ports only.** Ports {% for port in nodes_spec.allowed_ports %}`{{ port }}`{% if not loop.last %} and {% endif %}{% endfor %} and nothing else.
|
|
- **The donor enforces it too.** A client refuses a request that breaks these rules even when the server asks for it, so a compromised server cannot aim a donor's machine at their own network. The refusal codes are listed below.
|
|
- **Opt-in.** A node never connects unless the account holder granted the consent. Withdrawing it disconnects every node of that account.
|
|
- **No cookies, no accounts.** A node never attaches the donor's cookies or logins to a request.
|
|
|
|
## Version
|
|
|
|
Protocol version `{{ nodes_spec.version }}`, oldest supported `{{ nodes_spec.min_supported }}`. A client announces its version in `hello`; an unsupported version is closed with code `4400`.
|
|
|
|
## Transports
|
|
|
|
| Mode | Used by | Who can read the content | Trust |
|
|
|------|---------|--------------------------|-------|
|
|
| `tunnel` | native clients | nobody on the way: TLS runs end to end between DevPlace and the destination, through the donor's socket | tampering is impossible |
|
|
| `fetch` | browser extension | the donor can read and could forge the response | verified by structure checks and sampled redundancy, labelled `fetch` in results |
|
|
|
|
## Frames
|
|
|
|
Every frame is one JSON object with a `type`. An unknown type, a missing required field, an unexpected field or an oversize frame closes the socket with a reason. Direction `c2s` is client to server, `s2c` is server to client.
|
|
|
|
| Type | Direction | Max bytes | Fields |
|
|
|------|-----------|-----------|--------|
|
|
{% for frame in nodes_spec.frames %}| `{{ frame.type }}` | {{ frame.direction }} | {{ frame.max_bytes }} | {% for item in frame.fields %}`{{ item.name }}`: {{ item.type }}{% if not item.required %} (optional){% endif %}{% if not loop.last %}, {% endif %}{% else %}none{% endfor %} |
|
|
{% endfor %}
|
|
|
|
## Close codes
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
| `4013` | this worker does not own the node socket, reconnect immediately |
|
|
| `4029` | the network is at its connection capacity |
|
|
| `4400` | protocol error or unsupported version |
|
|
| `4403` | kicked: consent withdrawn, device revoked or suspended |
|
|
| `1008` | policy: not authenticated |
|
|
| `1009` | frame too large |
|
|
|
|
## Limits
|
|
|
|
Every number is a hard ceiling. Nothing is unbounded and nothing autotunes.
|
|
|
|
| Limit | Value |
|
|
|-------|-------|
|
|
{% for name, value in nodes_spec.limits.items() %}| `{{ name }}` | {{ value }} |
|
|
{% endfor %}
|
|
|
|
## Destination allowlist
|
|
|
|
| Host suffix | Adapter | Methods | Ports | Modes | Max requests per second |
|
|
|-------------|---------|---------|-------|-------|-------------------------|
|
|
{% for item in nodes_spec.destinations %}| `{{ item.suffix }}` | {{ item.adapter }} | {{ item.methods|join(", ") }} | {{ item.ports|join(", ") }} | {{ item.modes|join(", ") }} | {{ item.max_rps }} |
|
|
{% endfor %}
|
|
|
|
A host matches a suffix when it equals it or ends with `.` plus the suffix. `evilreddit.com` and `reddit.com.evil.net` do not match `reddit.com`.
|
|
|
|
## Refusal codes
|
|
|
|
A client reports one of these in `job_error` when it declines a job.
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
{% for item in nodes_spec.refusals %}| `{{ item.code }}` | {{ item.meaning }} |
|
|
{% endfor %}
|
|
|
|
## Donor-side checks
|
|
|
|
Before any connection a client resolves the host itself, refuses private, loopback, link-local, CGNAT and multicast addresses, then connects to the address it resolved. It follows no redirect that leaves the allowlist. It applies the donor's own bandwidth, concurrency and schedule limits locally, even when the server sends larger ones.
|
|
|
|
## Related
|
|
|
|
- [Community nodes API](/docs/nodes.html)
|
|
- [Privacy](/docs/privacy.html)
|
|
</div>
|