Local control API

July 15, 2026 ยท View on GitHub

This document defines the API implemented by the local control server. It is not a remote-management feature.

For tools that consume an OpenAPI description, use openapi.yaml. For the browser interface, see the local control dashboard guide.

Start it with the same named-tunnel configuration used by the CLI:

dgramtunneler control serve --port 8765

Transport and security boundary

  • Bind only to 127.0.0.1 by default; IPv6 loopback support may be added alongside it.
  • Do not accept a non-loopback bind address in this API version.
  • The packaged dashboard is served from the same local origin at /.
  • Use JSON request and response bodies, with UTF-8 encoding.
  • Prefix every endpoint and WebSocket message with API version v1.
  • Remote exposure, authentication, origin policy, and CSRF controls are deliberately deferred to Task 33.

REST resources

MethodPathPurpose
GET/api/v1/healthRead service readiness and API version.
GET/api/v1/tunnelsList saved tunnel aliases and equivalent direct commands.
GET/api/v1/tunnels/{alias}Read one saved tunnel definition.
POST/api/v1/tunnels/{alias}/startStart a named tunnel.
POST/api/v1/tunnels/{alias}/stopRequest graceful stop for a named tunnel.
POST/api/v1/tunnels/{alias}/restartStop, wait for, and restart a named tunnel.
POST/api/v1/tunnels/{alias}/producer/startStart a dummy producer using a client tunnel.
POST/api/v1/tunnels/{alias}/producer/stopRequest graceful stop for that producer.
POST/api/v1/tunnels/{alias}/producer/restartRestart a producer with supplied options.
GET/api/v1/runtimesList live tunnel and producer snapshots.
GET/api/v1/configRead the active TOML configuration.
PUT/api/v1/configValidate and replace the active TOML configuration.

State-changing calls return 202 Accepted with { "accepted": true }. POST action bodies are empty unless stated otherwise.

Producer request

POST /api/v1/tunnels/{alias}/producer/start accepts:

{
  "interval_milliseconds": 1000,
  "count": 10,
  "payload_prefix": "Dummy datagram"
}

Omit count to produce until stopped. interval_milliseconds must be positive.

Example runtime response

{
  "alias": "example-client",
  "kind": "tunnel",
  "state": "running",
  "detail": "Running",
  "metrics": {
    "datagram_count": 18424,
    "byte_count": 342091,
    "throughput_bytes_per_second": 43827.1,
    "average_latency_milliseconds": 0.21,
    "p50_latency_milliseconds": 0.20,
    "p99_latency_milliseconds": 0.62,
    "maximum_latency_milliseconds": 0.71
  }
}

Metrics that are not available, including latency for a producer or unsynchronised peer clocks, are null.

Each tunnel runtime also exposes recent_datagrams: the most recent ten observations, each containing a timestamp, byte length, and latency when the server can calculate it. Datagram payloads are never retained or returned.

WebSocket events

GET /api/v1/events upgrades to a WebSocket. The server emits event messages only; the client does not issue lifecycle commands over the socket.

{
  "api_version": "v1",
  "event": {
    "kind": "lifecycle",
    "severity": "info",
    "timestamp_unix_milliseconds": 1784125687123,
    "alias": "example-client",
    "message": "Running",
    "snapshot": { "alias": "example-client", "kind": "tunnel", "state": "running" }
  }
}

Event kinds are lifecycle, log, and metrics. A new connection receives a complete runtime snapshot before subsequent events, so the UI can recover after reconnecting.

While one or more WebSocket clients are connected, the server emits a bounded metrics snapshot for each active runtime once per second. These snapshots include aggregate metrics and the current rolling recent_datagrams buffer; they are not emitted once per forwarded datagram. This permits one WebSocket to carry telemetry for multiple tunnels without unbounded traffic. GET /runtimes remains the initial-load and reconnect fallback.

Errors

Every non-success response uses this shape:

{ "error": { "code": "not_found", "message": "Tunnel 'missing-link' does not exist" } }
HTTP statusError codeMeaning
400invalid_requestMalformed JSON or unsupported field/value.
404not_foundAlias or route does not exist.
409conflictRequested runtime is already active or cannot transition.
422validation_failedTOML or producer options are syntactically valid but invalid for DatagramTunneler.
500internalUnexpected server failure; no internal details are exposed.

Compatibility

v1 is additive within a major application release: optional response fields may be added, but existing fields retain their meaning. Breaking API changes require a new path version such as /api/v2.