mercuriod
The Mercurio MQTT broker daemon. It
serves MQTT 3.1, 3.1.1 and 5.0 over TCP, TLS and WebSocket. To try it, the
recado client publishes and subscribes
from the command line.
Installation
Or build from the workspace root:
Usage
| Option | Description | Default |
|---|---|---|
-c, --config <FILE> |
Path to configuration file | /etc/mercurio/config.toml |
-l, --listen <ADDR> |
Override the listen address, such as 0.0.0.0:1883 |
- |
-v, --verbose |
Log at debug level | - |
Without a configuration file at the default path, the broker starts with
the defaults below. A missing file at a path given with -c is an error.
Configuration
Mercuriod reads a TOML file. Every setting is optional; this is the full
list with the defaults, also available as
contrib/config.toml:
# Merge the *.toml files of this directory, in name order: their tables
# extend this file's, their [[listeners]] and [[bridges]] are added, and
# their other values replace this file's
# include_dir = "/etc/mercurio/conf.d"
[]
# Host address to bind to
# Use 0.0.0.0 to listen on all interfaces
= "127.0.0.1"
# Port to listen on (default MQTT port is 1883)
= 1883
# Only accept connections arriving on this network interface (Linux only).
# IPv6 hosts, such as "::", work as they are
# bind_interface = "eth0"
# Also serve MQTT over WebSocket (with TLS when it is enabled) on this port
# websocket_port = 8080
# Serve the files of this directory to plain HTTP requests on websocket_port,
# such as a browser client's page; [[listeners]] take it too
# http_dir = "/var/www/mercurio"
# Seconds between updates of the broker statistics published under
# $SYS/broker/ (0 disables them)
= 10
# Maximum number of concurrent connections
= 10000
# Largest packet accepted from a client, in bytes
= 1048576
# Messages that may wait for one client (QoS 1 and 2 included) before
# further ones are dropped
= 10000
# Queued messages beyond which QoS 0 messages are dropped
= 1000
# Payload bytes that may wait for one client before further messages are
# dropped, whatever their QoS (unlimited by default)
# max_queued_bytes = 104857600
# Send messages at the QoS of the subscription even when they were published
# with a lower one; goes beyond the MQTT specification
# upgrade_outgoing_qos = false
# Send a retained message to new subscribers only while the client that
# published it may still publish to its topic, so access revoked in the ACL
# also stops its retained messages
# check_retain_source = false
# Seconds a new connection has to complete TLS and send CONNECT
= 10
# Highest topic alias MQTT 5 clients may use (0 disables topic aliases)
= 10
# QoS 2 messages an MQTT 5 client may have awaiting PUBREL at once
= 65535
# Longest keep-alive, in seconds, MQTT 5 clients may use; clients asking for
# more, or for none, are told to use this instead
# max_keepalive = 300
# Response topic prefix given to MQTT 5 clients that request Response
# Information; %c is replaced by the client id
# response_information = "responses/%c"
# Replace repeated topics with aliases for MQTT 5 clients that accept them,
# saving bandwidth. Off by default because some client libraries announce
# alias support they don't implement.
# send_topic_aliases = true
# Highest QoS clients may publish and subscribe with; higher subscriptions
# are downgraded, higher publishes cost the connection
# max_qos = 2
# Set to false to refuse retained messages
# retain_available = true
# Accept clients that send an empty client id, assigning them one
# allow_zero_length_clientid = true
# Prefix of the client ids the broker assigns
# auto_id_prefix = "auto-"
# Only accept client ids starting with one of these
# clientid_prefixes = ["sensor-", "auto-"]
# Seconds the session of an MQTT 3.x client that connected without clean
# session outlives its connection; forever when unset, as MQTT 3.x has no
# session expiry of its own
# persistent_client_expiration = 604800
# Send every client to another server instead of serving it. MQTT 5 clients
# are told where to go; MQTT 3.x clients only learn this server is unavailable.
# [server.redirect]
# server_reference = "broker2.example.com:1883"
# permanent = false
[]
# Serve TLS on `port` (and on `websocket_port`) instead of plain TCP
= false
# Certificate chain and private key, in PEM format
# cert_path = "/etc/mercurio/server.crt"
# key_path = "/etc/mercurio/server.key"
# Require clients to present a certificate signed by this CA (mutual TLS)
# ca_path = "/etc/mercurio/ca.crt"
# Reject client certificates revoked in these CRLs (PEM); needs ca_path
# crl_path = "/etc/mercurio/crl.pem"
# Use the client certificate's Common Name as the user name, instead of
# the user name and password the client sends; needs ca_path
# identity_as_username = false
# Or the whole certificate subject, as in "CN=alice,O=Example,C=PT"
# subject_as_username = false
# Oldest TLS version accepted: "1.2" or "1.3"
# min_version = "1.2"
[]
# Log level: trace, debug, info, warn, error. Levels can also be set per
# module, as in "info,mercurio_server::session=warn"
= "info"
# Append logs to this file instead of standard output. Rotate it with
# logrotate's copytruncate, as the file stays open
# file = "/var/log/mercurio/mercuriod.log"
# Send logs to the local syslog daemon (/dev/log) instead
# syslog = false
# Log clients connecting and disconnecting; turn off on busy brokers
# connection_messages = true
[]
# Where sessions, retained messages, wills and in-flight messages are kept:
# "memory" (lost on restart) or "sqlite" (kept in the file at `path`)
= "memory"
# path = "/var/lib/mercurio/mercurio.db"
[]
# Require clients to authenticate
= false
# Allow clients that send no user name (when auth is enabled)
= true
# User names and Argon2 password hashes, managed with `mercuriod passwd`
# password_file = "/etc/mercurio/passwd"
# Restrict the topics each client may publish and subscribe to, using
# mosquitto's acl_file format. Anything not granted is denied. User rules
# only apply to user names verified by authentication; other clients are
# matched as anonymous.
# acl_file = "/etc/mercurio/acl"
# Manage clients, roles and access at runtime instead, with the commands of
# mosquitto's dynamic security plugin; replaces password_file and acl_file.
# Create the file with `mercuriod dynsec-init -f <path>`.
# dynamic_security = "/var/lib/mercurio/dynamic-security.json"
# Or delegate checks to an HTTP service: a 2xx answer allows
# [auth.http]
# user_url = "http://auth.internal/mqtt/user"
# acl_url = "http://auth.internal/mqtt/acl"
# timeout_seconds = 5
# cache_seconds = 30
# Extra listeners, each with its own protocol ("mqtt" or "websocket") and
# TLS settings, on a TCP `port` (`host` defaults to 127.0.0.1) or a Unix
# socket `path`.
# [[listeners]]
# host = "0.0.0.0"
# port = 8883
# [listeners.tls]
# enabled = true
# cert_path = "/etc/mercurio/server.crt"
# key_path = "/etc/mercurio/server.key"
#
# [[listeners]]
# port = 8080
# protocol = "websocket"
#
# Unix domain socket, for clients on the same machine
# [[listeners]]
# path = "/run/mercurio/mqtt.sock"
#
# Any TCP listener may also set max_connections, on top of the broker-wide
# limit, and bind_interface
# [[listeners]]
# host = "0.0.0.0"
# port = 8884
# max_connections = 100
# bind_interface = "eth1"
#
# Behind a load balancer that sends the PROXY protocol (v1 or v2), so logs
# show the clients' own addresses
# [[listeners]]
# host = "0.0.0.0"
# port = 1884
# proxy_protocol = true
#
# A tenant's listener: its clients only see topics under the mount point
# and log in with their own password file. `[listeners.auth]` takes the
# same settings as `[auth]` and replaces it for this listener.
# [[listeners]]
# host = "0.0.0.0"
# port = 1885
# mount_point = "tenant1/"
# [listeners.auth]
# enabled = true
# allow_anonymous = false
# password_file = "/etc/mercurio/tenant1.passwd"
# Bridges forward topics to and from another broker
# [[bridges]]
# name = "cloud"
# host = "broker.example.com"
# port = 8883
# client_id = "mercurio-bridge-cloud"
# username = "site1"
# password = "secret"
# Instead of host and port: brokers tried in turn when one is unreachable
# addresses = ["broker1.example.com:8883", "broker2.example.com:8883"]
# "mqttv5" (default) or "mqttv311" for brokers without MQTT 5
# protocol_version = "mqttv5"
# Publish "1"/"0", retained, to $SYS/broker/connection/<client id>/state
# notifications = true
# [bridges.tls]
# enabled = true
# ca_path = "/etc/mercurio/cloud-ca.crt"
# Client certificate, for brokers that require mutual TLS
# cert_path = "/etc/mercurio/site1.crt"
# key_path = "/etc/mercurio/site1.key"
#
# Each topic is forwarded "out" (the default), "in" or "both" ways.
# `pattern` is matched below `local_prefix` here and `remote_prefix` there.
# [[bridges.topics]]
# pattern = "sensors/#"
# direction = "out"
# qos = 1
# local_prefix = ""
# remote_prefix = "site1/"
Authentication
With [auth] enabled = true, clients must log in with a user name and
password from password_file, unless allow_anonymous lets clients that
send no user name in. Credentials that are sent are always checked.
The password file holds one user:hash line per user, with Argon2 hashes.
Files written by mosquitto_passwd work too: their $7$ (PBKDF2-SHA512)
and $6$ (SHA-512) hashes are recognised.
Manage it with:
# Add a user, or change their password (prompts for it)
# Remove a user
Access control
acl_file restricts which topics each client may publish and subscribe to,
in the format of mosquitto's acl_file, so existing files can be reused:
# Rules before any `user` line apply to anonymous clients
topic read public/#
user alice
topic readwrite alice/#
topic deny alice/secret
# Patterns apply to every client; %u is the user name, %c the client id
pattern write devices/%c/status
The access is read, write, readwrite (the default) or deny.
Anything not granted is denied, and deny overrides any grant. A
subscription is allowed only if a rule covers its whole filter: a client
granted sensors/+/temperature may subscribe to it, but not to
sensors/#. Shared subscriptions are checked by their topic filter:
$share/group/sensors/+/temperature needs the same grant.
Dynamic security
dynamic_security replaces password_file and acl_file with clients,
roles and access rules that change while the broker runs, as with
mosquitto's dynamic security plugin. Create the file with an administrator:
The administrator sends commands, in the JSON format of mosquitto's
plugin, to $CONTROL/dynamic-security/v1, and reads the answers on
$CONTROL/dynamic-security/v1/response:
Supported commands: createClient, deleteClient, setClientPassword,
enableClient, disableClient, addClientRole, removeClientRole,
getClient, listClients, createRole, deleteRole, addRoleACL,
removeRoleACL, getRole, listRoles, setDefaultACLAccess and
getDefaultACLAccess, and for groups createGroup, deleteGroup,
addGroupRole, removeGroupRole, addGroupClient, removeGroupClient,
getGroup, listGroups, setAnonymousGroup and getAnonymousGroup. A
client has its own roles and those of its groups; clients without a user
name get those of the anonymous group. ACLs of type publishClientSend
decide publishing, publishClientReceive receiving, and
subscribePattern and subscribeLiteral subscribing. The highest
priority wins. Without a matching ACL, only receiving is allowed, unless
setDefaultACLAccess changes it. Changes are saved to the file at once.
$CONTROL topics are only open to clients when access control grants
them, so a broker without acl_file or dynamic_security refuses them.
HTTP authentication
[auth.http] delegates checks to an HTTP service, as mosquitto-go-auth's
HTTP backend does. For each login, the broker POSTs
{"username", "password", "clientid"} to user_url; clientid is
always empty. For each topic check, it POSTs
{"username", "clientid", "topic", "acc"} to acl_url, with acc 1 to
receive, 2 to publish and 4 to subscribe. A 2xx status allows; anything else, a timeout
included, denies. Topic answers are reused for cache_seconds, since
every message is checked. user_url replaces password_file and
acl_url replaces acl_file.
Reloading
Send SIGHUP (systemctl reload mercuriod) to reread password_file,
acl_file and dynamic_security without dropping connections. New ACL
rules apply to every later publish and subscribe, including those of
connected clients. Password changes apply to later logins. If a file fails
to load, the broker logs the error and keeps the previous version. Other
settings still need a restart.
TLS
Set enabled = true under [server.tls] with a certificate and key to serve
TLS on port, and on websocket_port when it is set. With ca_path, the
broker also requires a client certificate signed by that CA.
crl_path rejects revoked client certificates. With
identity_as_username, the certificate's Common Name becomes the client's
user name: it counts as authenticated, and user rules in acl_file
apply to it. The user name and password in CONNECT are then ignored.
subject_as_username uses the whole subject instead, written as RFC 2253
does and as mosquitto's use_subject_as_username gives it:
CN=alice,O=Example,C=PT.
min_version = "1.3" refuses TLS 1.2.
WebSocket
websocket_port serves MQTT over WebSocket, using the mqtt subprotocol,
for browsers and clients behind HTTP proxies. Its sessions, retained
messages and subscriptions are shared with the TCP listener.
Listeners
Each [[listeners]] entry serves another port, with its own protocol
(mqtt or websocket) and its own [listeners.tls] settings. A typical
setup keeps plain MQTT on localhost in [server] and adds a TLS listener on
0.0.0.0:8883. All listeners share sessions, subscriptions and retained
messages.
mount_point puts a listener's clients in their own part of the topic
tree: a client publishing sensors/t1 on a listener mounted at
tenant1/ publishes tenant1/sensors/t1, and it is sent topics without
the prefix. [listeners.auth] gives a listener its own authentication and
acl_file, which SIGHUP reloads like the main ones. ACL rules see the
full topic, mount point included.
A listener with path instead of port serves a Unix domain socket, which
local clients reach without TCP and which file permissions can restrict.
A socket file left behind by a broker that did not stop cleanly is
replaced.
Bridges
A [[bridges]] entry connects to another broker and forwards topics between
the two, like mosquitto's bridges. Each [[bridges.topics]] entry
forwards pattern out, in or both ways. The prefixes rename
topics on the way: with local_prefix = "site/" and
remote_prefix = "plant1/", site/sensors/t1 here is
plant1/sensors/t1 there.
With MQTT 5, bridges use No Local subscriptions so that forwarded
messages never come back. MQTT 3.1.1 has no No Local, so with
protocol_version = "mqttv311" the bridge recognises the copies the remote
broker sends back and drops them; an identical message published on the
remote broker just after one was forwarded is dropped too. With
addresses, the bridge moves on to the next broker whenever it can't
connect or loses the connection. With notifications, the bridge's state
is published, retained, to $SYS/broker/connection/<client id>/state:
1 while connected, 0 otherwise. The bridge
connects to this broker in-process, so authentication and acl_file don't
apply to it. If the remote broker is unreachable, the bridge retries every
five seconds, and messages to forward wait in the bridge's queue on this
broker, up to max_queued_messages. Its session there lasts a day, so messages for in topics
wait for the bridge across short outages.
Persistence
With backend = "memory", everything is lost when the broker stops. With
backend = "sqlite", sessions, subscriptions, retained messages, wills,
queued messages and messages in flight are kept in the database file, so
clients with persistent sessions resume where they left off after a
restart.
Statistics
Every sys_interval seconds the broker publishes retained statistics under
$SYS/broker/, with mosquitto's names: version, uptime,
clients/connected, clients/disconnected, clients/total,
clients/maximum, clients/expired, messages/received,
messages/sent, bytes/received, bytes/sent,
retained messages/count and subscriptions/count, and the 1, 5 and 15
minute averages per minute of messages, bytes and connections under
load/, such as load/messages/received/1min. Clients
may subscribe to them, but not publish to $SYS topics.
Running
Direct execution
# With the default config location
# With a custom config file
# Override the listen address
# Debug logging
Ctrl+C or SIGTERM shuts the broker down gracefully: clients are sent a DISCONNECT saying the server is shutting down, and the broker waits up to five seconds for their connections to close before exiting.
Systemd service
A systemd unit is provided in contrib/mercuriod.service.
It runs the broker sandboxed, with /var/lib/mercurio as its only
writable directory, which suits the SQLite database.
# Copy service file
# Create config directory and copy config
# Copy binary
# Enable and start service
# Check status
# View logs
Features
See the project README for the full list of supported MQTT features.