7.4.6. Web UI WebSocket Protocol
The built-in LN manager browser UI uses normal HTTP plus a JSON-over-WebSocket protocol on the same manager TCP port.
This section documents that protocol for users who want to implement their own websocket-based interfaces.
7.4.6.1. Transport
The manager-side entry point is the same port configured by the instance
setting manager.
Connection flow:
the client performs an HTTP request, typically
GET / HTTP/1.1HTTP Basic Auth is required unless the client already has a valid session cookie
after successful authentication, the client requests a WebSocket upgrade
after the upgrade, all application messages are JSON objects in WebSocket text frames
The built-in browser UI also uses lightweight focused WebSocket modes selected via query parameters on the upgrade request:
/ws?single_process=PROCESS/NAME: subscribe only to one process and its output stream/ws?single_group=GROUP/NAME: subscribe only to one recursive group subtree and the output streams of its contained processes/ws?log_only=1: subscribe only to manager log messages
7.4.6.2. HTTP Authentication
The built-in browser UI uses the auth file configured by webui_auth_file or
LNM_WEBUI_AUTH_FILE.
Authentication rules:
if neither is configured, HTTP is refused with a plain-text explanation
if the auth file is group/other-readable, HTTP is refused with a plain-text explanation
the initial browser login uses HTTP Basic Auth
after successful authentication,
ln_managerreturns a cookie:Set-Cookie: ln_manager_web_session=<random-token>; Path=/; HttpOnly; SameSite=Lax
that random token is persisted in
<authfile>.sessionslater HTTP and WebSocket requests can authenticate only with that cookie
HTTP keep-alive is supported for plain HTTP requests, so custom clients should prefer reusing the same TCP connection instead of reconnecting for every asset or metadata request.
7.4.6.3. Raw HTTP Log Endpoint
Besides the browser WebSocket UI, the manager also exposes a plain-text log endpoint:
GET /lnm/log?raw
Authentication is the same as for the browser UI: either HTTP Basic Auth or a valid session cookie.
Supported query parameters:
level=debug|info|warning|error: minimum log level to returnlimit=N: return only the lastNmatching in-memory log entriesfollow: keep the HTTP response open and stream future matching log entries using HTTP chunked transfer encoding
Without limit, the manager returns all currently retained in-memory log
entries matching the requested level.
Example one-shot fetch:
wget -q --user=USER --password=PASSWORD -O - \
"http://<IP>:<PORT>/lnm/log?raw&level=debug&limit=15"
Example follow mode:
wget -q --user=USER --password=PASSWORD -O - \
"http://<IP>:<PORT>/lnm/log?raw&level=info&follow"
7.4.6.4. Save-All HTTP Endpoint
The diagnostics archive is deliberately exposed as plain HTTP, not as a WebSocket frame:
GET /save-all
Authentication is the same as the browser UI. The response body is binary
.tar.bz2 data and includes a Content-Disposition: attachment header so
browsers treat it as a file download.
Supported query parameters:
name=NAME: optional sanitized name hint for the top-level archive prefix and suggested download filename
Example:
wget --user=USER --password=PASSWORD -O debug.tar.bz2 \
"http://<IP>:<PORT>/save-all?name=my_debug_report"
7.4.6.5. LN Daemon Logger HTTP Endpoint
LN daemon logger downloads are plain HTTP downloads, not WebSocket frames.
They retrieve data from daemon-side per-port ring buffers and are unrelated to
lnrecorder:
GET /lnm/logger-download?name=LOGGER&format=pickle&stop=1
Authentication is the same as the browser UI.
Supported query parameters:
name=LOGGER: name of the manager-side LN daemon logger objectformat=pickle|matlab|raw: output format;pickleis the defaultstop=1: stop the logger before collecting and downloading data
The response body is binary data with a Content-Disposition: attachment
header. The server sends large downloads asynchronously in chunks so the
manager does not fail on short non-blocking socket writes.
7.4.6.6. JSON Message Format
After WebSocket upgrade, every application frame is a JSON object with a
top-level type field.
7.4.6.6.1. Manager to client messages
Common manager-to-client frame types are:
snapshotprocess_treeprocess_upsertprocess_removeprocess_outputprocess_output_snapshotclients_snapshotclient_upsertclient_removeprocess_listprocess_list_task_set_resultprocess_attribute_set_resultprocess_startup_commandstopic_upserttopic_removeservice_upsertservice_removeservices_snapshotparameters_snapshotparameter_override_resultparameter_open_scope_resultlog_batchloggers_snapshotlogger_upsertlogger_removelogger_action_resultnetwork_statsnetwork_topologyerror
7.4.6.6.2. Client to manager messages
Common client-to-manager frame types are:
get_snapshotstart_processstop_processreload_configget_process_outputsubscribe_process_outputunsubscribe_process_outputsend_stdinsend_winchstart_groupstop_groupset_process_current_alternate_commandset_process_command_overrideset_process_alternate_command_overrideset_process_no_skip_displayset_process_low_delay_outputset_process_attributedump_startup_commandsrequest_clientsrequest_process_listset_process_list_taskrequest_logsset_log_min_levelrequest_topics_statrequest_topic_detailrequest_topic_schedulingapply_topic_schedulingrequest_parametersset_parameter_overrideopen_parameter_scoperequest_network_statsrequest_network_topologyrequest_loggersconfigure_loggerstart_loggerstop_loggerdelete_loggerrequest_config_filesave_config_file
7.4.6.7. Process Snapshot Example
Initial state is typically fetched with:
{"type": "get_snapshot"}
The manager answers with a large snapshot object. A shortened example:
{
"type": "snapshot",
"manager": {
"name": "test instance",
"host": "rmc-lx0031"
},
"hosts": ["rmc-lx0031"],
"host_list": [
{
"name": "rmc-lx0031",
"aliases": ["rmc-lx0031.example.org"],
"node_aliases": ["localhost"],
"all_names": ["localhost", "rmc-lx0031", "rmc-lx0031.example.org"],
"daemon_ready": true
}
],
"start_tools": [
{"value": null, "label": "default"},
{"value": "gdb", "label": "gdb"},
{"value": "valgrind", "label": "valgrind"}
],
"process_tree": [
{
"type": "group",
"name": "all",
"display_name": "all",
"opened": true,
"children": [
{"type": "process", "name": "all/publish"},
{"type": "process", "name": "all/subscribe"}
]
}
],
"processes": [
{
"name": "all/publish",
"display_name": "publish",
"state": "ready",
"requested_state": "start",
"pid": 3208721,
"host": "rmc-lx0031",
"node": "localhost",
"command": "/usr/bin/python3 publish.py 100",
"command_override": null,
"alternate_commands": [
["default", "/usr/bin/python3 publish.py 100"],
["htop", "/usr/bin/htop"]
],
"alternate_command_overrides": {},
"current_alternate_command": "default",
"start_tool": null,
"ln_debug": false,
"current_command": "/usr/bin/python3 publish.py 100",
"has_ready_state": true,
"start_time": "2026-04-21T14:11:38.521003",
"stop_time": null
}
],
"clients": [
{
"id": 1,
"name": "trace_test43",
"pid": 3208721,
"host": "rmc-lx0031",
"address": "10.0.0.7:46122",
"library_version": 16,
"svn_revision": 1234,
"modified": false,
"ln_debug": false,
"process_name": "all/publish",
"published_topics": ["/topic/demo"],
"subscribed_topics": [],
"provided_services": []
}
]
}
Note that process_upsert does not contain accumulated process output. That
data is intentionally streamed separately.
hosts is the compatibility list of canonical host names. Prefer
host_list for new clients that need host-name aliases, node-name aliases,
or current daemon readiness.
7.4.6.8. Daemon Process Lists
The browser UI exposes daemon process/task listings through
/lnm/process-list. PID/TID links in the UI open that route with query
parameters such as:
/lnm/process-list?host=HOST&pid=PID&tid=TID
The underlying WebSocket request is:
{
"type": "request_process_list",
"host": "HOST",
"show_threads": true,
"show_env": false,
"filter": ""
}
The manager answers with process_list. The payload contains ordered column
metadata and row values with both raw and formatted values. These lists can be
large; clients should avoid rendering all rows synchronously.
filter is passed to the daemon as a structured process-list filter, not as
free text. Each item is encoded as key:op:value and multiple items are
separated by NUL bytes. For example, use tid:==:1234 to request one Linux
task/thread.
Scheduling, priority, and affinity changes use:
{
"type": "set_process_list_task",
"host": "HOST",
"pid": 1234,
"tid": 1234,
"policy": 1,
"prio": 20,
"affinity": "0,1"
}
7.4.6.9. Clients
Client state is also available explicitly:
{"type": "request_clients"}
The manager responds with:
{
"type": "clients_snapshot",
"clients": []
}
Incremental changes arrive as client_upsert and client_remove.
7.4.6.10. Parameters
LN parameter providers can be queried explicitly:
{
"type": "request_parameters",
"pattern": "*"
}
The manager responds with formatted values:
{
"type": "parameters_snapshot",
"pattern": "*",
"parameters": [
{
"id": "7:ln.parameters.example.cpp.query_dict:ln.parameters.example.cpp.sig1",
"name": "ln.parameters.example.cpp.sig1",
"service": "ln.parameters.example.cpp.query_dict",
"provider": "ln_parameters_example",
"provider_id": 7,
"description": "sig1 desc",
"input": "1.0",
"output": "1.0",
"override_value": "1.0",
"override_enabled": false
}
],
"errors": []
}
Set or clear one override with:
{
"type": "set_parameter_override",
"service": "ln.parameters.example.cpp.query_dict",
"provider_id": 7,
"parameter": "ln.parameters.example.cpp.sig1",
"enabled": true,
"value": "42.0"
}
The result frame is parameter_override_result.
Request publication of one parameter as a topic and open it in ln_scope:
{
"type": "open_parameter_scope",
"service": "ln.parameters.example.cpp.query_dict",
"provider_id": 7,
"parameter": "ln.parameters.example.cpp.sig1"
}
The manager calls the provider’s matching request_topic service and then
opens <parameter>.output in the configured scope process. The result frame is
parameter_open_scope_result.
The stdlib-only reference client exposes convenience wrappers for these messages:
from links_and_nodes_manager_client import ManagerClient
async with ManagerClient("http://localhost:43809") as client:
parameters = await client.get_parameters("*")
parameter = parameters[0]
await client.set_parameter_override(
parameter["service"],
parameter["name"],
True,
"42.0",
provider_id=parameter["provider_id"],
)
await client.open_parameter_scope(
parameter["service"],
parameter["name"],
provider_id=parameter["provider_id"],
)
7.4.6.11. Process Control Examples
Start a process:
{"type": "start_process", "name": "all/publish"}
Stop a process:
{"type": "stop_process", "name": "all/publish"}
Start all processes in a group:
{"type": "start_group", "name": "all/subgroup1"}
Stop all processes in a group:
{"type": "stop_group", "name": "all/subgroup1"}
Select an alternate command:
{
"type": "set_process_current_alternate_command",
"name": "all/publish",
"which": "htop"
}
Override the primary command:
{
"type": "set_process_command_override",
"name": "all/publish",
"override": "/usr/bin/python3 publish.py 200"
}
Override a non-default alternate command:
{
"type": "set_process_alternate_command_override",
"name": "all/publish",
"which": "htop",
"override": "/usr/bin/htop -d 20"
}
Set process UI/runtime attributes such as the selected start tool or
LN_DEBUG:
{
"type": "set_process_attribute",
"name": "all/publish",
"attribute": "start_tool",
"value": "gdb"
}
attribute is currently limited to start_tool and ln_debug.
To inspect the generated startup command sequence without injecting it into the live terminal:
{"type": "dump_startup_commands", "name": "all/publish"}
7.4.6.12. Terminal Output and Input
To fetch initial terminal backlog for a process:
{
"type": "get_process_output",
"name": "all/publish",
"lines": 500
}
The manager answers with a complete snapshot for that process terminal:
{
"type": "process_output_snapshot",
"name": "all/publish",
"pid": 3208721,
"output": "ready\r\n"
}
This snapshot request does not subscribe the connection to future live output.
Live process output is opt-in per WebSocket connection. Subscribe with one or
more fnmatch.fnmatch process-name patterns:
{
"type": "subscribe_process_output",
"patterns": ["all/publish", "tools/*"]
}
Exact process names are valid patterns. A client that wants all live process
output can subscribe to "*". To stop receiving matching live output,
remove the same patterns:
{
"type": "unsubscribe_process_output",
"patterns": ["tools/*"]
}
After subscription, live output for matching process names arrives incrementally:
{
"type": "process_output",
"name": "all/publish",
"output": "new line of output\r\n"
}
Keyboard input is sent to the process via:
{
"type": "send_stdin",
"name": "all/publish",
"text": "q"
}
Terminal resize is sent with:
{
"type": "send_winch",
"name": "all/publish",
"rows": 40,
"cols": 120
}
The built-in web UI uses these messages to mirror the GTK VTE behavior.
7.4.6.13. Python Reference Client
python/links_and_nodes_manager_client contains the stdlib-only Python
3.11+ reference implementation for this HTTP/WebSocket interface. It is meant
both as a usable asyncio client and as demo code for custom integrations. Keep
it feature-complete when adding new HTTP routes or WebSocket message types.
It can also be installed with pip directly from this repository subdir:
python3 -m pip install ./python/links_and_nodes_manager_client
After that, both the importable package and the lnm-client console script
are available.
Example:
import asyncio
from links_and_nodes_manager_client import ManagerClient
async def main():
async with ManagerClient("localhost:43809") as lnm:
await lnm.get_snapshot()
await lnm.start_process("all/publish")
output = await lnm.get_process_output("all/publish")
print(output["output"])
asyncio.run(main())
For simple scripts that should not manage an asyncio loop, use
SyncManagerClient:
from links_and_nodes_manager_client import SyncManagerClient
with SyncManagerClient("localhost:43809") as lnm:
snapshot = lnm.get_snapshot()
print("processes:", len(snapshot.get("processes", [])))
output = lnm.get_process_output("all/publish")
print(output["output"])
SyncManagerClient is a generated wrapper around ManagerClient. It does
not implement a second HTTP/WebSocket protocol stack; each synchronous method
calls the corresponding async method and runs an internal asyncio event loop
until that call is complete. This makes it easier for beginners and small
automation scripts while keeping ManagerClient the maintained reference
implementation. Code that already runs inside asyncio should use
ManagerClient directly instead.
The package also provides a demo CLI:
export LN_MANAGER=localhost:43809
python3 -m links_and_nodes_manager_client host-list
python3 -m links_and_nodes_manager_client processes --watch
python3 -m links_and_nodes_manager_client process-list localhost --filter htop
python3 -m links_and_nodes_manager_client output -f all/publish
The CLI and library can use explicit credentials, LN_MANAGER_USER /
LN_MANAGER_PASSWORD, or LNM_WEBUI_AUTH_FILE. When only a username is
specified and the auth file exists, the password is read from that file. When
no username is specified, the first auth-file entry is used. The file must not
be readable by group or others.
7.4.6.13.1. Proxy support
The reference client honors http_proxy / https_proxy / all_proxy
and supports HTTP CONNECT plus SOCKS4/SOCKS4A/SOCKS5 proxies without external
dependencies. This is useful for reaching a manager port through an SSH jump
host:
ssh -N -D 127.0.0.1:1080 user@jump-host
In another shell:
export all_proxy=socks5h://127.0.0.1:1080
export LN_MANAGER=manager-host-visible-from-jump:43809
python3 -m links_and_nodes_manager_client processes
python3 -m links_and_nodes_manager_client logs -f
socks5h:// asks the proxy side to resolve the manager hostname. Use
socks5:// only when local DNS resolution is desired. To bypass the proxy
for directly reachable hosts, use no_proxy:
export no_proxy=localhost,127.0.0.1,manager-on-local-network
7.4.6.14. Topics, Services and Logs
Topic updates:
{
"type": "topic_upsert",
"topic": {
"name": "topic1",
"publisher": "publish",
"publisher_host": "rmc-lx0031",
"subscribers": 1,
"publisher_rate": 100.0,
"published_packets": 95006,
"message_definition": "uint32_t",
"message_size": 1984
}
}
Service updates:
{
"type": "service_upsert",
"service": {
"name": "publish.request_topic",
"interface": "service/request_topic",
"provider": "publish",
"provider_host": "rmc-lx0031",
"port": 46321,
"unix_socket": null
}
}
Log batches:
{
"type": "log_batch",
"entries": [
{
"id": 184,
"ts": "2026-04-21T14:11:39.771621",
"level": "info",
"source": "Manager",
"msg": "process 'all/publish' changes state from 'started' to 'ready'"
}
]
}
LN daemon logger snapshot:
{
"type": "loggers_snapshot",
"loggers": [
{
"name": "experiment1",
"enabled": true,
"active_topics": ["topic1"],
"topics": [
{
"name": "topic1",
"log_size": 10000,
"divisor": 1,
"only_ts": false,
"enabled": true,
"active": true,
"n_samples": 321,
"md_name": "uint32_t",
"port": "shm:1234 on rmc-lx0031"
}
]
}
]
}
Configure an LN daemon logger:
{
"type": "configure_logger",
"name": "experiment1",
"only_ts": false,
"topics": [
{"name": "topic1", "log_size": 10000, "divisor": 1}
]
}
Start or stop it:
{"type": "start_logger", "name": "experiment1"}
{"type": "stop_logger", "name": "experiment1"}
Changes arrive as logger_upsert / logger_remove. Command results arrive
as logger_action_result.
Network usage sampling:
{"type": "request_network_stats", "sample_seconds": 2.0, "sample_mode": "interval"}
The manager answers asynchronously with network_stats. sample_seconds
controls the packet/byte counter sampling interval. sample_mode can be
interval or previous; previous compares against the last manager-side
network sample and does not sleep. If no previous sample exists yet, the manager
falls back to a 2 second interval sample. The payload includes
combined logical host links, measured per-interface traffic, per-row
children breakdowns for topics, service-call accounting, TCP forwards,
daemon-manager connections, client-manager connections, lnm_remote
GUI/console connections, and web UI HTTP/WS traffic, and possible problems when
measured traffic is more than 5 percent below the theoretical value.
links is an endpoint view: it attributes traffic to the logical source and
destination hosts and does not include intermediate forwarding hops as separate
host-link rows. Physical hop/interface traffic is represented by the
interfaces rows instead.
links_theoretical and links_measured are still sent for compatibility,
but new clients should use links:
{
"type": "network_stats",
"ok": true,
"stats": {
"sample_seconds": 2.0,
"links": [
{
"from_host": "host1",
"to_host": "host2",
"bytes_s": 1048576.0,
"packets_s": 1000.0,
"measured_bytes_s": 996147.0,
"measured_packets_s": 950.0,
"children": [
{
"kind": "topic",
"name": "topic1",
"bytes_s": 1048576.0,
"packets_s": 1000.0,
"measured_bytes_s": 996147.0,
"measured_packets_s": 950.0
}
]
}
],
"interfaces": [],
"problems": [
{
"severity": "warning",
"message": "the link between host1 and host2 seems to be saturated, overused, or lossy"
}
]
}
}
Network topology:
{"type": "request_network_topology"}
The manager answers with network_topology containing configured networks and
gateway hosts:
{
"type": "network_topology",
"ok": true,
"topology": {
"networks": [
{
"name": "net1",
"netmasks": ["10.0.0.0/24"],
"interfaces": [{"host": "host1", "interface": "eth0", "ip": "10.0.0.1"}]
}
],
"gateways": []
}
}
7.4.6.15. Error Frames
Protocol or execution errors are reported as:
{
"type": "error",
"message": "unknown action 'frobnicate'"
}
7.4.6.16. Implementation Notes
The built-in implementation currently lives in:
manager side:
python/links_and_nodes_manager/web_server/websocket helper:
python/links_and_nodes_base/websocket_protocol.pybrowser UI:
python/links_and_nodes_manager/resources/html/app.js
That code is the source of truth if the documentation and implementation ever diverge.