11 KiB
Control API
Purpose
pkg/controlapi exposes a localhost JSON and WebSocket API so applications in any language can use Reticulum destinations, announces, links, and requests without embedding the Go transport stack.
The server is optional and disabled by default.
Architecture notes
The mesh is destinations, announces, and links between peers. No node is privileged on the wire. Upstream design intent: Zen of Reticulum.
The Control API is a local HTTP/WebSocket front end for one reticulum-go process. It is not the mesh. Apps that treat this API as a required remote service reintroduce a single control host even when RNS routing stays peer-to-peer.
Appropriate uses
- Tools and UIs on the same host as the daemon
- App logic in another language while the daemon owns transport
- Lab or ops access on loopback (or a private network you fully control)
Avoid
- A public Control API endpoint that clients must use to participate
- Putting identity, routing, or app policy behind one always-on control host
- Large transfers via base64
link.send_resourcewhen rncp or in-process librns is available - Binding off loopback and describing the result as decentralized because RNS is underneath
If the product fails when the Control API host is unreachable, the product depends on that host. Prefer peer destinations and links for application traffic. Keep this API on the machine that runs the node.
Enable in config
[reticulum]
enable_control_api = yes
rpc_key = <64 hex characters>
control_api_host = 127.0.0.1
control_api_port = 37430
# Optional extra listener. TCP stays enabled.
# control_api_socket = /run/reticulum-go/control.sock
rpc_key is a 32-byte value encoded as hex. The same key authenticates shared-instance RPC when configured.
Generate a key with a cryptographic random source. Example using OpenSSL:
openssl rand -hex 32
Authentication
All /v1 routes require:
Authorization: Bearer <hex rpc_key>
Requests without a valid bearer token are rejected.
HTTP routes
| Method | Path | Description |
|---|---|---|
| GET | /v1/health |
Liveness probe (process up, transport id, uptime). Not mesh integrity scoring |
| GET | /v1/status |
Interface statistics, including Go local integrity counters when present |
| GET | /v1/paths |
Path table snapshot |
| POST | /v1/sessions |
Create session (identity) |
| DELETE | /v1/sessions/{id} |
Tear down session |
| POST | /v1/sessions/{id}/destinations |
Register destination |
| POST | /v1/sessions/{id}/destinations/{hash}/announce |
Send announce |
| POST | /v1/sessions/{id}/destinations/{hash}/requests |
Bridge request path to WebSocket |
| DELETE | /v1/sessions/{id}/destinations/{hash}/requests?path= |
Deregister request path |
| POST | /v1/sessions/{id}/path/request |
Request path to destination |
| GET | /v1/sessions/{id}/events |
WebSocket event stream |
Lifecycle routes (Go node integration):
| Method | Path | Description |
|---|---|---|
| POST | /v1/lifecycle/resume |
Resume after pause |
| POST | /v1/lifecycle/pause |
Pause interfaces |
| POST | /v1/lifecycle/refresh-paths |
Refresh stale paths |
Binary fields (hashes, app data, link payloads) are hex- or base64-encoded as documented in pkg/controlapi/protocol.go.
Status integrity fields (Go daemon)
GET /v1/status mirrors shared-instance interface stats. Against a Reticulum-Go daemon each interface object may include:
| JSON field | Meaning |
|---|---|
| ifac_fail | IFAC verify failures |
| hmac_fail | Link HMAC failures |
| announce_sig_fail | Invalid announce signatures |
| unpack_fail | Packet unpack failures |
| announce_dup | Duplicate announce ignored |
| path_resp_suppressed | PATH_RESPONSE skipped (next hop is requestor) |
| path_req_dup | Duplicate path request tag ignored |
| path_req_no_cache | Known path without cached announce |
| path_resp_queued_skip | PATH_RESPONSE already queued for iface |
| link_relay_unknown_iface | Link relay dropped unknown source iface |
| integrity_fail_rate | Windowed fails / (fails + accepted) |
| stale_closes | Links closed after going stale |
| link_stale_close | Same lifetime total as exposed on the iface |
| keepalive_timeout | Transitions into keepalive stale |
| clients | Spawned peer count (I2P parent) |
| i2p_connectable | Connectable I2P server tunnel enabled |
| i2p_b32 | Published *.b32.i2p endpoint when connectable |
| tunnelstate | I2P peer tunnel label (Creating Tunnel, Tunnel Active, Tunnel Unresponsive) |
| i2p_last_error | Last SAM dial or stream error text for an I2P peer |
These counters are local observability only. They do not change packet accept or reject policy. For scored findings use reticulum-go slow. For a full path and health dump use reticulum-go snapshot. See Security, packet-debug, and CLI utilities.
Sessions
A session owns:
- One identity
- Destinations registered under that identity
- Links opened or accepted by the session
Typical flow:
POST /v1/sessions
-> session id
POST /v1/sessions/{id}/destinations
-> register app destination
POST /v1/sessions/{id}/destinations/{hash}/announce
-> publish announce
GET /v1/sessions/{id}/events (WebSocket)
-> subscribe to announces, links, requests
WebSocket events
Server to client JSON event type values:
| Event | Meaning |
|---|---|
| announce | Remote announce received |
link.established |
Link is active |
link.failed |
Outbound link failed |
link.data |
Data received on link |
link.closed |
Link closed |
link.remote_identified |
Peer identified on link |
request.incoming |
Request arrived on registered path |
request.response |
Outbound link.request succeeded |
request.failed |
Outbound link.request failed or timed out |
resource.started |
Resource transfer started |
resource.concluded |
Resource transfer finished |
command.error |
WebSocket command could not be applied |
Client to server command type values:
| Command | Meaning |
|---|---|
| subscribe_announces | Subscribe to announces. Empty filter means all. Non-empty filter must be an exact 16-byte dest hash hex |
link.open |
Open outbound link |
link.send |
Send on link |
link.close |
Close link |
link.request |
Outbound request on established link |
link.send_resource |
Send payload as a link resource (base64). Keep payloads small |
link.identify |
Identify session identity on link |
request.respond |
Answer a request. Optional filename for NomadNet [name, bytes] |
Full type definitions: pkg/controlapi/protocol.go.
Links via API
Register a destination with link acceptance enabled for inbound links.
Outbound: send link.open over the events WebSocket after the path exists (from announce or path request).
Both directions receive link.established when ready, then link.data for peer data.
Use link.identify after the link is active. The peer receives link.remote_identified.
Requests via API
Register a request path with POST .../destinations/{hash}/requests. Incoming requests appear as request.incoming. Respond with request.respond before the handler timeout.
Outbound: after link.established, send link.request. Completion arrives as request.response or request.failed.
Handlers block the underlying link goroutine until response or timeout. Keep processing short.
Deregister with DELETE .../requests?path=/your/path.
Resources via API
link.send_resource mirrors librns minimal resource send. Expect resource.started and resource.concluded on the peer. Payloads are base64 over WebSocket, so large files are memory-heavy. Prefer rncp or in-process librns for bulk transfers.
Scope and caveats
This API is an application contract for destinations, announces, links, requests, identify, and minimal resources. It is not a full mirror of channels, stream buffers, resource cancel/progress, or mesh-admin ops (drop path, blackhole). Those stay on shared-instance RPC and CLI.
Control API /v1 is independent of librns RNS_API_VERSION. Additive JSON fields and new type strings are the compatibility model.
WebSocket event delivery is best-effort. A full client outbox drops events.
See Architecture notes when designing a product on top of this API.
Example client
examples/control-client/client.py is a Python reference client for the API.
Security notes
- Default bind is loopback only
- Do not expose the control API to untrusted networks without additional protection
- Treat rpc_key as a secret comparable to an API token
- Binding off loopback for convenience fights the model in Architecture notes
- See Security
Implementation files
| File | Role |
|---|---|
server.go |
HTTP server and routing |
session.go |
Session state |
protocol.go |
Request and event types |
ws.go |
WebSocket handling |
auth.go |
Bearer validation |
lifecycle.go |
Lifecycle routes |
Daemon wiring: cmd/reticulum-go/main.go starts controlapi.Server when enabled.
Related documents
- Zen of Reticulum (upstream design intent)
- API reference for Go embedders using destinations and links in-process
- Configuration
- Links, channels, and resources
- librns for in-process C ABI
- librns for in-process Odin bindings
- Examples
Dart and Flutter
Path: bindings/dart/ (package rns_control).
Control API client
HTTP and WebSocket client for a local or LAN reticulum-go daemon. Import package:rns_control/rns_control.dart.
import 'package:rns_control/rns_control.dart';
final client = ControlClient(rpcKey: rpcKey);
final session = await client.createSession();
final events = client.openEvents(session.sessionId);
events.subscribeAnnounces();
Coverage includes health, status (with integrity counters), paths, sessions, destinations, announce, request handlers (register and deregister), lifecycle, outbound requests, resources, identify, and WebSocket commands or events. Authenticated WebSocket upgrades require dart:io (Flutter mobile or desktop). Browser clients cannot set the Authorization header on WebSocket.
In-process FFI
For embedding without a daemon, use package:rns_control/ffi.dart over librns on Linux, Android, and Windows. See librns Dart FFI.
task build-librns
task test-dart
# or
make -C bindings/dart test
Add to a Flutter app with a path dependency:
dependencies:
rns_control:
path: ../Reticulum-Go/bindings/dart