Node network protocol
This page is generated from the protocol specification, services/nodes/spec.py, the same data served at /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 and /nodes/spec.json exist. The node socket, the device registration, the consent flow and the clients are not implemented yet. Nothing on this page is enabled for any account.
Rules that never change#
- Not an open proxy.
open_proxyisfalse. A node only ever contacts hosts on the allowlist below. Changing that is a protocol change, not a setting. - Web ports only. Ports
80and443and 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 1, oldest supported 1. 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 |
|---|---|---|---|
hello |
c2s | 262144 | protocol: int, client_version: str, kind: str, platform: str, modes: list, limits: object (optional) |
pong |
c2s | 262144 | none |
job_result |
c2s | 3145728 | job_id: str, status: int, headers: object (optional), body_b64: str, truncated: bool (optional), elapsed_ms: int (optional) |
job_error |
c2s | 262144 | job_id: str, code: str |
tunnel_data |
c2s | 262144 | conn_id: str, b64: str |
tunnel_close |
c2s | 262144 | conn_id: str |
tunnel_opened |
c2s | 262144 | conn_id: str, ok: bool, code: str (optional) |
usage |
c2s | 262144 | bytes_up: int, bytes_down: int |
welcome |
s2c | 262144 | device_uid: str, limits: object, destinations: list, settings: object |
ping |
s2c | 262144 | none |
job |
s2c | 262144 | job_id: str, method: str, url: str, headers: object (optional), timeout: int |
tunnel_open |
s2c | 262144 | conn_id: str, host: str, port: int |
tunnel_data |
s2c | 262144 | conn_id: str, b64: str |
tunnel_close |
s2c | 262144 | conn_id: str |
settings_update |
s2c | 262144 | settings: object |
kick |
s2c | 262144 | reason: str |
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 |
|---|---|
max_devices_per_user |
5 |
max_connected_nodes |
2000 |
max_frame_bytes |
262144 |
max_body_bytes |
2097152 |
max_concurrent_per_node |
2 |
min_seconds_per_node_domain |
6 |
job_timeout_seconds |
25 |
max_queue_depth |
500 |
default_wait_seconds |
15 |
max_wait_seconds |
60 |
default_daily_requests_user |
200 |
default_daily_requests_guest |
0 |
default_donor_mb_per_day |
500 |
max_donor_mb_per_day |
20480 |
ping_seconds |
20 |
missed_pings_before_evict |
2 |
reconnect_min_seconds |
1 |
reconnect_max_seconds |
60 |
ticket_ttl_seconds |
30 |
cache_ttl_seconds |
600 |
cache_max_bytes |
268435456 |
cache_entry_max_bytes |
4194304 |
usage_daily_retention_days |
400 |
event_retention_days |
30 |
event_max_rows |
50000 |
request_retention_seconds |
600 |
xp_daily_cap |
50 |
dispatcher_tick_ms |
200 |
Destination allowlist#
| Host suffix | Adapter | Methods | Ports | Modes | Max requests per second |
|---|---|---|---|---|---|
reddit.com |
GET | 80, 443 | tunnel, fetch | 0.5 | |
redd.it |
GET | 80, 443 | tunnel, fetch | 0.5 |
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 |
|---|---|
host_not_allowed |
destination is not on the allowlist |
port_not_allowed |
port is not 80 or 443 |
address_not_public |
resolved address is private, loopback, link-local, CGNAT or multicast |
redirect_not_allowed |
redirect leaves the allowlist |
method_not_allowed |
method is not permitted for the destination |
bandwidth_exhausted |
donor daily bandwidth cap reached |
concurrency_exceeded |
donor concurrency cap reached |
outside_hours |
outside the donor's active hours |
body_too_large |
response exceeds the body cap |
timeout |
job exceeded its timeout |
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.