Administration

Look at and change a running broker: its clients and sessions, bans, retained messages and subscriptions, its log level and files, without restarting it.

The broker takes JSON commands on the MQTT topic $CONTROL/broker/v1, from any MQTT 5 client that access control allows, and from anyone on its host who may use its admin socket. mercurioctl runs each of these commands from the command line. This page describes the commands, who may run them, and what changes for setups upgrading from earlier versions.

The admin socket

[admin]
socket = "/run/mercurio/admin.sock"   # "" turns it off
group = "mercurio"

mercuriod serves this Unix socket by default. Clients connecting through it skip authentication and access control: they may run every command, dynamic security's included. So the broker checks who connects, from the user and group the kernel reports for the socket, and closes everyone else at once:

The socket file belongs to the broker's user and to group, with mode 0660. With group = "", or a group that doesn't exist, it is 0600 and only root and the broker's user get in.

The packages run the broker as the mercurio user, and its systemd unit creates /run/mercurio, so the defaults work as installed. To let a user administer the broker:

sudo usermod -aG mercurio alice
sudo systemctl restart mercuriod

The broker reads the group's members when it starts, so a new member needs a restart of the broker, and a new login before their own processes carry the group. Changes to [admin] also need a restart.

With another socket path, every directory on it must be writable only by root or the broker's user: anyone who can write to one of them could put their own socket in its place. The broker refuses a socket whose directory belongs to another user or is writable by its group or by others; the directories above it are up to you. A broker that can't create its socket, such as one started by hand where /run/mercurio doesn't exist, logs a warning and runs without it. A socket file left by a broker that stopped without removing it is replaced; one where another process still accepts connections, such as a second broker started with the same configuration, stops the new broker with an error instead. For a broker run as yourself:

mkdir -m 700 -p "$XDG_RUNTIME_DIR/mercurio"
[admin]
socket = "/run/user/1000/mercurio/admin.sock"

The admin socket needs Linux; elsewhere [admin] is ignored with a warning.

Over the network

Any MQTT client may send commands once access control allows it. Like every $CONTROL topic, $CONTROL/broker/v1 is refused unless a rule grants it. Each command is also checked on its own, as a publish to $CONTROL/broker/v1/<command>: a client granted $CONTROL/broker/v1/listClients may list clients and nothing else. The client must also be allowed to subscribe to, and receive, the topic its answers come on.

With an ACL file:

# Every command
user ops
topic readwrite $CONTROL/broker/v1/#

# Only listing and showing clients
user monitor
topic write $CONTROL/broker/v1
topic write $CONTROL/broker/v1/listClients
topic write $CONTROL/broker/v1/getClient
topic read $CONTROL/broker/v1/response/#

write alone isn't enough: without read, subscribing to the answers is refused.

With dynamic security, mercuriod dynsec-init gives its admin role publishClientSend, subscribePattern and publishClientReceive on $CONTROL/broker/v1/#; give other roles the same ACLs, or narrower ones. With HTTP authentication, acl_url is asked about $CONTROL/broker/v1 and $CONTROL/broker/v1/<command> with acc 8, and about the answer topic with 4 and 1; a user its superuser_url names a superuser is allowed all of these without asking. A plugin that allows every topic grants none of this: publishing to $CONTROL is a separate question, which only an explicit grant answers yes to.

Bridges and other in-process connections run every command without checks, as the admin socket's clients do.

The protocol

A request is a JSON object with a list of commands, published to $CONTROL/broker/v1. The commands run in order, and the answer holds one response for each, in the same order:

mosquitto_rr -V mqttv5 -u ops -P "$OPS_PASSWORD" \
    -t '$CONTROL/broker/v1' -e '$CONTROL/broker/v1/response/ops' \
    -m '{"commands": [
          {"command": "getLogLevel", "correlationData": "1"},
          {"command": "listBans", "count": 2},
          {"command": "nope"}
        ]}'
{"responses":[{"command":"getLogLevel","correlationData":"1","data":{"level":"info"}},
 {"command":"listBans","data":{"bans":[],"totalCount":0}},
 {"command":"nope","error":"unknown command `nope`","code":"invalidArgument"}]}

Errors

codeMeans
notAuthorizedAccess control doesn't grant $CONTROL/broker/v1/<command>
notFoundWhat the command names, such as a client, a ban or a retained message, doesn't exist
invalidArgumentAn unknown command, an argument the command doesn't take, such as a misspelt one, or a missing or malformed argument
conflictThe command can't run in the current state, such as deleting the session of a connected client
unavailableThe broker can't serve it now: storage failed, a wait timed out, or the command is served by mercuriod and the broker runs without it

A client that may not publish to $CONTROL/broker/v1 at all gets no answer; at QoS 1, its PUBACK says Not Authorized.

Lists

List commands take offset, 0 by default, and count, 100 by default and at most 1000. Beside the items they return totalCount, the length of the whole list. The rest of a list of 250 clients is {"command": "listClients", "offset": 100}, then "offset": 200.

Commands

Arguments go in the command object beside command; those marked ? are optional, with their default in parentheses. A client is any session, connected or not. Times are Unix seconds, and durations seconds. Dynamic security has its own commands, on $CONTROL/dynamic-security/v1.

Clients and sessions

CommandArgumentsReturns
listClientsconnected?, clientidPrefix?, username?, address? (an IP address or CIDR range), mountpoint? (the mount point of the client's listener, "" for none), offset?, count?clients, by client id, then mount point: clientid, mountpoint (null for a listener without one), username, connected, address, listener, protocolVersion, connectedSince or disconnectedSince, and the counts subscriptions, queued, inflight, and dropped: messages for the client the broker lost since it last connected, those lost while it is away included (see $SYS statistics for the causes)
getClientclientid, mountpoint?The same fields, with subscriptions listing each filter with its qos, noLocal, retainAsPublished and subscriptionIdentifier; and keepAlive, cleanStart, sessionExpiry (null for never), sessionExpiresIn while disconnected, willPending, messagesIn, messagesOut, bytesIn, bytesOut, dropped. notFound without such a session.
disconnectClientclientid, mountpoint?, sendWill? (true), reasonCode? (152, 0x98 Administrative Action)Nothing. Closes the connection; MQTT 5 clients get a DISCONNECT with the reason code, which must be an error code the MQTT 5 specification lets a server send in a DISCONNECT, such as 0x8B or 0x98; invalidArgument for any other. The session stays. notFound if the client isn't connected.
deleteSessionclientid, mountpoint?, disconnect? (false)Nothing. Removes the session with its subscriptions, queued messages and pending will. conflict while the client is connected, which includes the client deleting its own session, unless disconnect is true, which first closes it with 0x98 and without its will; unavailable if closing takes more than 5 seconds.

Each mount point has its own client ids, so one id can name a session on each. getClient, disconnectClient and deleteSession take the client's mountpoint, "" for a listener without one. Without it they act on the one mount point the id is on, and fail with invalidArgument, listing the mount points, when it is on more than one:

{"command": "deleteSession", "clientid": "sensor-7", "mountpoint": "tenant-a/"}

Bans

CommandArgumentsReturns
addBantype (clientid, username or address), value, prefix? (false; clientid and username only, invalidArgument with address), duration? (never expires), reason?Nothing. See Bans.
removeBantype, value, prefix? (false)Nothing. notFound without such a ban.
listBansoffset?, count?bans: type, value, prefix, reason, createdAt, expiresAt (null for never)

Retained messages

CommandArgumentsReturns
listRetainedfilter? (#), offset?, count?messages, by topic: topic, qos, size in bytes, expiresIn (null for never), and preview, the first 64 bytes as text, null when they aren't UTF-8. As in subscriptions, # doesn't match $SYS topics.
getRetainedtopictopic, qos, expiresIn, the payload as payload when it is UTF-8 or as payloadBase64 otherwise, and properties: payloadFormatIndicator, contentType, responseTopic, correlationDataBase64 and userProperties, each when set. notFound without one.
deleteRetainedfilterdeleted: how many. A topic deletes its own message; a filter with wildcards deletes every one it matches.

Subscriptions

CommandArgumentsReturns
listSubscriptionsfilterPrefix?, offset?, count?subscriptions, sorted: each filter with its number of subscribers. Shared subscriptions appear as $share/<group>/<filter>.
getSubscribersfiltersubscribers of exactly that filter: clientid, mountpoint, qos, noLocal, retainAsPublished, subscriptionIdentifier. Not paged.

The broker

CommandArgumentsReturns
getBrokerInfoversion, startedAt, uptime, storage (memory, sqlite or postgres), and stats: the $SYS statistics by their topic under $SYS/broker/
listListenersoffset?, count?listeners, the main one first: address or path, protocol, tls, mountPoint, connections
listPluginsoffset?, count?plugins: name, kind (builtin, rust or wasm), hooks. WebAssembly plugins add interface, the version of the plugin interface they were built for, such as 0.2.0, instances, failures since the broker started (traps, calls past timeout_ms and calls the broker stopped at plugin_timeout), and lastError.

mercuriod serves the following ones; a broker embedded without them answers unavailable.

CommandArgumentsReturns
listBridgesoffset?, count?bridges: name, connected, remoteAddress, connectedSince or disconnectedSince, queued, the messages waiting for the bridge on this broker, forwardedOut and forwardedIn, the messages the other broker accepted in each direction since the broker started, and dropped, those refused by the other broker or covered by no route
getConfigThe configuration in use, with the keys of the TOML file. Passwords, credentials in URLs and every plugin config table read "<redacted>".
getLogLevellevel
setLogLevellevel: directives as [logging] level takes themNothing. The level lasts until the broker restarts. invalidArgument for an empty or malformed level.
reloadRereads the files SIGHUP rereads, and returns items, one per file: item, the plugin it configures (password-file, acl-file, dynamic-security or a [[plugins]] name), tls for a listener's TLS files, or http-auth for the certificate files of [auth.http], path, the certificate's for tls and the CA's, or else the client certificate's, for http-auth, ok, and error when it failed. A file that failed keeps its previous contents.

Bans

A ban refuses clients by client id, user name or address, until it expires or is removed:

{"command": "addBan", "type": "address", "value": "203.0.113.0/24", "duration": 3600, "reason": "flooding"}

Upgrading from mercuriod 0.4.5 or earlier

Upgrading from mercuriod 0.4.3 or earlier