mercuriod 0.5.0

Mercurio MQTT broker daemon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
# mercuriod

The [Mercurio](https://github.com/frenzox/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`](https://crates.io/crates/recado) client publishes and subscribes
from the command line, and
[`mercurioctl`](https://crates.io/crates/mercurioctl) administers it while
it runs.

## Installation

Each [release](https://github.com/frenzox/mercurio/releases) has `.deb`
and `.rpm` packages for x86_64, aarch64 and armv7 Linux, which install the
binary, the configuration in `/etc/mercurio/` and the systemd unit:

```bash
sudo apt install ./mercuriod_*.deb
sudo dnf install ./mercuriod-*.rpm
```

Or with cargo:

```bash
cargo install mercuriod
```

Or build from the workspace root:

```bash
cargo build --release --package mercuriod
```

## Usage

```bash
mercuriod [OPTIONS]
mercuriod passwd -f <FILE> -u <USER> [-d]
mercuriod dynsec-init -f <FILE> [-u <USER>]
mercuriod dynsec-import <MOSQUITTO FILE> <FILE>
```

| 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 | - |
| `--check` | Check the configuration and exit | - |

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.

`mercuriod --check` does what a start does short of listening and opening
storage: it reads the configuration and its `include_dir`, loads the
password, ACL and dynamic security files, the TLS certificates and keys,
and the WebAssembly plugins, then exits with status 0. A problem makes it
exit with status 1 and the error a start would print. Run it as the
broker's user, so it reads the files as the broker would, before
restarting the broker after a change:

```bash
sudo -u mercurio mercuriod --check && sudo systemctl restart mercuriod
```

## Configuration

Mercuriod reads a TOML file. Every setting is optional, and a key it does
not know stops it from starting, with an error naming the key. This is the
full list with the defaults, also available as
[`contrib/config.toml`](contrib/config.toml):

```toml
# Merge the *.toml files of this directory, in name order: their tables
# extend this file's, their [[listeners]], [[bridges]] and [[plugins]] are
# added, and their other values replace this file's
# include_dir = "/etc/mercurio/conf.d"

[server]
# Host address to bind to
# Use 0.0.0.0 to listen on all interfaces
host = "127.0.0.1"

# Port to listen on (default MQTT port is 1883)
port = 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)
sys_interval = 10

# Maximum number of concurrent connections
max_connections = 10000

# Largest packet accepted from a client, in bytes
max_packet_size = 1048576

# Largest CONNECT accepted from a client, in bytes. It comes before the
# client is authenticated, so it gets less room than other packets
max_connect_packet_size = 65536

# Messages that may wait for one client (QoS 1 and 2 included) before
# further ones are dropped
max_queued_messages = 10000

# Queued messages beyond which QoS 0 messages are dropped
max_queued_qos0_messages = 1000

# Payload bytes that may wait for one client before further messages are
# dropped, whatever their QoS
# max_queued_bytes = 104857600
# Which message a full queue drops: "drop_newest", the one arriving, or
# "drop_oldest", the oldest waiting of its kind (QoS 0, or QoS 1 and 2) so
# a client that falls behind gets the latest ones
# queue_overflow = "drop_newest"
# How a shared subscription's group picks the member a message goes to:
# "round_robin", "random", "sticky" (the same member for as long as it is
# connected and takes messages) or "hash" (by the publisher's client id, so
# each publisher's messages stay in order). Connected members whose queue
# has room always come first
# shared_subscription_strategy = "round_robin"
# When the messages a member was given and hasn't processed go to another
# member: those still queued for it and those at QoS 1 it hasn't
# acknowledged. "session_end" when the member's session ends; "disconnect"
# also as soon as it loses its connection, to the members connected then.
# QoS 2 messages already sent stay with the member. A QoS 1 message the
# member processed without acknowledging reaches the application twice
# shared_subscription_redelivery = "session_end"


# 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
# Subscriptions one session may hold; further ones get Quota Exceeded
# max_subscriptions = 10000
# Longest topic filter, in bytes, a SUBSCRIBE may name; longer ones get
# Topic Filter Invalid
# max_topic_filter_length = 1024

# Seconds a new connection has, in all, to send its PROXY header, complete
# the TLS and WebSocket handshakes and send CONNECT
connect_timeout = 10

# Seconds writing one packet to a client may take before its connection is
# dropped as stalled. A client reconnecting with the same client id waits as
# long for the old connection to close, then gets Server Busy
write_timeout = 10

# Highest topic alias MQTT 5 clients may use (0 disables topic aliases)
max_topic_alias = 10

# QoS 2 messages an MQTT 5 client may have awaiting PUBREL at once
receive_maximum = 65535

# QoS 1 and 2 messages sent to one client and not yet acknowledged, at most,
# whatever Receive Maximum the client announces; each holds its message in
# memory until acknowledged
max_inflight = 64

# 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, which are bare UUIDs without
# one
# 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
# Most seconds any session may outlive its connection, MQTT 5 clients
# included; a week by default, "unlimited" to keep sessions forever
# max_session_expiry = 604800
# Sessions outliving their connection the broker may hold, connected or
# not; a client that would start one more gets Quota Exceeded (MQTT 3.x:
# Server Unavailable). Resuming one is never refused, and sessions ending
# with their connection don't count (unbounded by default)
# max_sessions = 100000
# Topics that may hold a retained message at once, the broker's own $SYS
# topics not counted; further ones get Quota Exceeded (unbounded)
# max_retained_messages = 100000
# Connections one client address may hold at once, counting the address a
# PROXY header gives (unbounded by default, as NAT puts many behind one)
# max_connections_per_ip = 100
# Failed logins from one address (IPv6 by /64) within login_failure_window
# seconds that get it banned for login_failure_ban seconds. The ban is
# listed by `mercurioctl ban list` and lifted like any other; loopback
# addresses and the admin socket are never banned. 0 attempts turns it off
# login_failure_attempts = 10
# PUBLISH packets per second one client may send on average, and at once
# after a quiet spell (a second's worth by default). A client going faster
# isn't read from until its rate allows, so it slows down as TCP pushes
# back; nothing is dropped. In-process clients (bridges) and the admin
# socket are not limited. Unlimited by default
# max_publish_rate = 100
# publish_rate_burst = 500
# The same for the payload bytes one client publishes
# max_publish_bytes_rate = 1048576
# publish_bytes_burst = 4194304
# Seconds a client may stay over either rate, with more waiting each time
# the broker reads from it again, before it is disconnected with Message
# Rate Too High (MQTT 3.x clients are just disconnected). Clients are only
# slowed down by default
# publish_rate_grace = 30
# login_failure_window = 60
# login_failure_ban = 300
# Seconds one call of a plugin hook may take, for built-in authentication
# and access control as for [[plugins]]; a slower call denies, or drops the
# message. [auth.http] timeout_seconds and a plugin's timeout_ms may not
# exceed it
# plugin_timeout = 5
# Seconds between calls of the plugins' on_tick
# plugin_tick_interval = 1

# 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

[server.tls]
# Serve TLS on `port` (and on `websocket_port`) instead of plain TCP
enabled = 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"
# With ca_path, let clients without a certificate in to log in by password;
# a certificate that is presented must still be valid
# require_certificate = true
# Reject client certificates revoked in these CRLs (PEM); needs ca_path
# crl_path = "/etc/mercurio/crl.pem"
# Every certificate of a client's chain needs a CRL from its issuer in
# crl_path, or the client is refused. Check only the client's own
# certificate, leaving its intermediate CAs unchecked
# crl_end_entity_only = false
# Accept certificates whose issuer has no CRL in crl_path
# crl_allow_unknown = false
# 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"

[logging]
# Log level: trace, debug, info, warn, error. Levels can also be set per
# module, as in "info,mercurio_server::session=warn"
level = "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

[storage]
# Where sessions, retained messages, wills and in-flight messages are kept:
# "memory" (lost on restart), "sqlite" (kept in the file at `path`) or
# "postgres" (kept in the database at `url`)
backend = "memory"
# path = "/var/lib/mercurio/mercurio.db"
# url = "postgres://mercurio:password@localhost/mercurio?sslmode=require"
# Connections to another host, or with ca_path, are encrypted unless the URL
# sets sslmode=prefer or disable; the certificate and host name are verified
# CA that signed the PostgreSQL server's certificate; Mozilla's roots otherwise
# ca_path = "/etc/mercurio/postgres-ca.pem"
# One broker serves from a PostgreSQL database at a time. When another is,
# exit (false), or wait and take over once it stops (true)
# standby = false
# Connections each broker keeps to PostgreSQL, plus one that holds the
# database: the server's max_connections must allow pool_size + 1 for each
# broker, standbys included
# pool_size = 16
# Seconds a write waits for a free connection before it is tried again
# pool_timeout = 5

[auth]
# Require clients to authenticate. password_file, dynamic_security and
# user_url authenticate clients, so the broker refuses to start when one is
# set while this is false; acl_file and acl_url work either way
enabled = false

# Allow clients that send no user name (when auth is enabled)
allow_anonymous = false

# User names and Argon2 password hashes, managed with `mercuriod passwd`,
# which also stores SCRAM-SHA-256 keys so clients can log in without
# sending the password; entries set before, or by mosquitto_passwd, get
# them when their password is set again
# 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. acl_url alone,
# without authentication, only checks topic access
# [auth.http]
# user_url = "http://auth.internal/mqtt/user"
# acl_url = "http://auth.internal/mqtt/acl"
# A 2xx answer for a user name makes it a superuser: no acl_url check
# restricts it, and it may administer the broker over $CONTROL. Only with
# authentication enabled
# superuser_url = "http://auth.internal/mqtt/superuser"
# No longer than [server] plugin_timeout
# timeout_seconds = 5
# cache_seconds = 30
# For https:// endpoints: CA certificates their certificates must chain to,
# in place of the bundled roots, and a client certificate with its key for
# endpoints that ask for one. Each file is PEM.
# ca_path = "/etc/mercurio/auth-ca.pem"
# cert_path = "/etc/mercurio/auth-client.pem"
# key_path = "/etc/mercurio/auth-client-key.pem"

[admin]
# Unix socket for administering the broker from this machine, as mercurioctl
# does. Its clients skip authentication and access control, so only root,
# the broker's own user and the members of `group` may connect; others are
# closed at once. Every directory on a custom socket path must be writable
# only by root or the broker's user. "" turns it off. Linux only: elsewhere
# this is ignored.
socket = "/run/mercurio/admin.sock"
# Members are read when the broker starts; add an administrator with
# `usermod -aG mercurio <user>`. "" leaves only root and the broker's user.
group = "mercurio"

# 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]
# authentication enabled
# cert_path = "/etc/mercurio/server.crt"
# key_path = "/etc/mercurio/server.key"
#
# [[listeners]]
# port = 8080
# protocol = "websocket"
# Web pages from other origins are refused (browsers send the page's origin;
# other clients send none and are not affected); any origin when unset
# allowed_origins = ["https://app.example.com"]
#
# Health and Prometheus metrics endpoints over HTTP, on WebSocket listeners
# only and off by default: GET /healthz answers 200 while the broker serves,
# GET /metrics the statistics. Metrics tell anyone who reaches the listener
# how loaded the broker is, so give them a listener of their own on a
# loopback address
# [[listeners]]
# host = "127.0.0.1"
# port = 9883
# protocol = "websocket"
# health = true
# metrics = true
#
# Unix domain socket, for clients on the same machine. It belongs to the
# broker's user with mode 0600, so only that user may connect, unless
# `group` and `mode` open it to others (Linux only)
# [[listeners]]
# path = "/run/mercurio/mqtt.sock"
# Group the socket belongs to; the mode is then 0660 unless set
# group = "mqtt"
# Permission bits in octal
# mode = "0660"
#
# Any TCP listener may also set max_connections, on top of the broker-wide
# limit, bind_interface, and max_connection_rate, the new connections a
# second it accepts with connection_rate_burst at once, closing the others
# unanswered. Any listener may replace the [server] limits
# max_packet_size, max_connect_packet_size, max_keepalive and
# connect_timeout for its clients, and with use_username_as_clientid give
# each client the user name it authenticated as for client id, refusing
# clients without one
# [[listeners]]
# host = "0.0.0.0"
# port = 8884
# max_connections = 100
# bind_interface = "eth1"
# max_connection_rate = 50
# connection_rate_burst = 200
# max_packet_size = 65536
# max_connect_packet_size = 4096
# max_keepalive = 120
# connect_timeout = 5
# use_username_as_clientid = true
#
# 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
# Proxies allowed to send the header, as addresses or CIDR blocks; others
# are closed, as their header could claim any address. Required with
# proxy_protocol; ["0.0.0.0/0", "::/0"] lets any peer send one
# proxy_from = ["10.0.0.0/8"]
#
# 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/"
# Names of the [[plugins]] its clients go through; all of them when unset
# plugins = ["deny"]
# [listeners.auth]
# authentication enabled
# 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"
# Keep-alive with the remote broker, in seconds
# keepalive = 60
# Start a new session on the remote broker at each connection instead of
# resuming the one kept there
# clean_start = false
# Seconds the remote broker keeps the session, and messages for "in"
# topics, after the link drops
# session_expiry = 86400
# Publish "1"/"0", retained, to $SYS/broker/connection/<client id>/state
# notifications = true
# Another topic for the state
# notification_topic = "bridges/cloud/state"
# With false, publish the state on the remote broker too; it publishes "0"
# through the bridge's will when the link drops. Brokers that refuse
# client publishes to $SYS, Mercurio among them, need a notification_topic
# notifications_local_only = true
# Each bridge needs its own name and client id; with notifications and no
# notification_topic, the client id must be a single topic level, without
# `/`, `+` or `#`
# [bridges.tls]
# TLS with the system's root certificates
# authentication enabled
# A CA, or a client certificate, turns TLS on even without `enabled`
# ca_path = "/etc/mercurio/cloud-ca.crt"
# Client certificate, for brokers that require mutual TLS; both or neither
# 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/"

# WebAssembly plugins, run in this order after the built-in authentication
# and access control. Components built for plugin interface 0.2 or 0.1 load;
# listPlugins shows which. A plugin publishes as the in-process client
# mercurio-plugin-<name>, never to $ topics, 64 KiB a message and 100
# messages a second at most. SIGHUP rereads the file at `path`; other
# changes need a restart. See docs/plugins.html
# [[plugins]]
# path = "/etc/mercurio/plugins/deny_topic.wasm"
# Names the plugin in logs, in $CONTROL/<name>/ and in a listener's
# `plugins`; the file name without .wasm by default
# name = "deny"
# Strings passed to the plugin's init
# config = { prefix = "$internal/" }
# Hosts it may send HTTP requests to, as "host" (port 443) or "host:port"
# allow_http = ["audit.internal:80"]
# Addresses it may open sockets to, as "address:port"
# allow_sockets = ["10.0.0.5:6379"]
# Copies that run calls at once; the number of CPUs by default
# instances = 4
# Longest one call may run, in milliseconds; plugin_timeout by default, and
# no longer than it
# timeout_ms = 5000
# Memory each copy may use, in MiB
# memory_mb = 64
```

### 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. Any other hash field, such as a
plain-text password or an empty one, stops the file from loading, with an
error naming its line.
Manage it with:

```bash
# Add a user, or change their password (prompts for it)
mercuriod passwd -f /etc/mercurio/passwd -u alice

# Remove a user
mercuriod passwd -f /etc/mercurio/passwd -u alice -d
```

`passwd` replaces the file in one step, keeping its owner, group and mode
(at most 0640). A new file gets mode 0600: let the broker read it with
`chgrp mercurio` and `chmod 640`. It refuses to change a file it
cannot read in full, so fix a malformed line by hand first.

### 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:

```text
# 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:

```bash
sudo mercuriod dynsec-init -f /var/lib/mercurio/dynamic-security.json -u admin
```

The file is created with mode 0600. Run as root in a directory owned by
another user, as the service's state directory `/var/lib/mercurio` is, the
command gives the file to that directory's owner and group, so the service
can read it. systemd creates that directory when the service first starts:
start it once before, with `systemctl start mercuriod`, if it has never
run.

Coming from mosquitto, create it from the plugin's file instead:

```bash
sudo mercuriod dynsec-import /var/lib/mosquitto/dynamic-security.json \
    /var/lib/mercurio/dynamic-security.json
```

It carries over clients, groups, roles, ACLs, priorities, the default
access and the anonymous group, gives the file to its directory's owner as
`dynsec-init` does, and never overwrites a file. Passwords keep working:
mosquitto's PBKDF2-SHA512 hashes are checked as they are, and stored as
Argon2 once set again. What it leaves out, such as unsubscribe ACLs, it
lists.

The administrator manages it with `mercurioctl`, which names mosquitto's
commands in kebab-case, as `mosquitto_ctrl` does:

```bash
mercurioctl dynsec create-role sensor
mercurioctl dynsec add-role-acl sensor publishClientSend 'sensors/#' allow
mercurioctl dynsec create-client s1 --role sensor   # asks for the password
```

Underneath, these are JSON commands on `$CONTROL/dynamic-security/v1`, in
the format of mosquitto's plugin.

Supported commands: `createClient`, `modifyClient`, `deleteClient`,
`setClientPassword`, `enableClient`, `disableClient`, `addClientRole`,
`removeClientRole`, `getClient`, `listClients`, `createRole`, `modifyRole`,
`deleteRole`, `addRoleACL`, `removeRoleACL`, `getRole`, `listRoles`,
`setDefaultACLAccess` and `getDefaultACLAccess`, and for groups
`createGroup`, `modifyGroup`, `deleteGroup`,
`addGroupRole`, `removeGroupRole`, `addGroupClient`, `removeGroupClient`,
`getGroup`, `listGroups`, `setAnonymousGroup` and `getAnonymousGroup`. A
client created with a `clientid` may connect only with that client id. A
user name with no client, such as one from a client certificate, gets no
access at all; create a client of that name, without a password, to give
it roles. 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. As in mosquitto, a
check walks the client's own roles, then its groups' roles, group by group,
each list highest `priority` first (-1 when none is given), and within a
role its ACLs by their `priority`; the first ACL that matches decides.
Without a matching ACL, only receiving is allowed, unless
`setDefaultACLAccess` changes it. Changes are saved to the file at once.
As in mosquitto, a change that can take access away disconnects the
connected clients it concerns, with Administrative Action (0x98), so they
log in again under their new access.

`$CONTROL` topics are only open to clients when access control grants
them, so a broker without `acl_file` or `dynamic_security` refuses them
over the network. The admin socket, below, needs no grant.

### 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 the
one the client sent, empty when the broker is to assign one. 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. With `superuser_url`, the broker first POSTs
`{"username"}` there; a 2xx answer, cached like topic answers, makes the
user a superuser, allowed every topic and `$CONTROL` without asking
`acl_url`. Anything else, a failure included, means not a superuser. Over
`https://`, `ca_path` names the CA
certificates endpoints' certificates must chain to, in place of the bundled
roots, and `cert_path` and `key_path` a client certificate for endpoints
that ask for one. `user_url` replaces `password_file` and
`acl_url` replaces `acl_file`.

### Reloading

Send `SIGHUP` (`systemctl reload mercuriod`), or run
`mercurioctl broker reload`, to reread `password_file`,
`acl_file`, `dynamic_security`, the TLS `cert_path`, `key_path`, `ca_path`
and `crl_path` of each listener and the `.wasm` file of each `[[plugins]]`
entry without dropping connections. New ACL rules apply to every later
publish and subscribe, including those of connected clients. Password
and TLS changes apply to later connections; connected clients keep the
certificate they were served. A reloaded plugin takes over once its
`init` has run; calls already running finish on the old code. If a file
fails to load, the broker logs the error and keeps the previous version.
Other settings, the other `[[plugins]]` keys among them, still need a
restart.

### Administration

`mercurioctl` lists and disconnects clients, bans them, deletes sessions
and retained messages, shows subscriptions, changes the log level and
reloads files while the broker runs. On the broker's host it connects
through the admin socket set in `[admin]`, `/run/mercurio/admin.sock` by
default, which root, the broker's user and members of the `mercurio` group
may use without credentials. Over the network it needs a user that access
control grants `$CONTROL/broker/v1/#`. The
[administration guide](https://frenzox.github.io/mercurio/admin.html)
describes every command, its permissions and what changed since 0.4.

### 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.
`require_certificate = false` makes it optional: clients without one log
in by user name and password, while a certificate that is presented must
still be valid. `crl_path` rejects revoked client certificates. Each
certificate of the client's chain, intermediate CAs included, must be
covered by a CRL from its issuer, or the client is refused: give the CRLs
of the root and of every intermediate CA. `crl_end_entity_only = true`
checks only the client's own certificate, and `crl_allow_unknown = true`
accepts certificates whose issuer has no CRL in the file. 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,
except from clients that present no certificate where it is optional.
`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. A mount point must end with `/`, as `tenant1` alone would
also reach `tenant10/...`. `[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. Two bridges may not share a `name` or a
client id, as each would take over the other's session; with
`notifications`, the client id must be a single topic level. `tls.ca_path`
or a client certificate turns TLS on even without `tls.enabled`, as
`--cafile` does for recado, and `cert_path` and `key_path` go together. The
bridge
connects to this broker in-process, so authentication and `acl_file` don't
apply to it. A QoS 1 or 2 message is acknowledged to the broker it came
from only once the other broker accepted it, so a connection lost on either
side never loses it; it may arrive twice instead. Up to 100 messages are in
flight in each direction, within the receiving broker's Receive Maximum, and
keep their order. If the remote broker is unreachable, the bridge retries after
one second, doubling the wait after each failed attempt up to a minute, with
some randomness, 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"` or `backend = "postgres"`, sessions, subscriptions,
retained messages, wills, queued messages and messages in flight are kept in
the database, so
clients with persistent sessions resume where they left off after a
restart. Queued QoS 1 and 2 messages of persistent sessions are kept whether
their client is connected or not, each removed as it is sent, in the same
write that records it in flight, or dropped; QoS 0 messages are not kept.

Changes are written in the background within milliseconds, so a crash loses
only the changes of its last moments; a shutdown writes everything first.
Changes the database fails to take are retried; when it keeps refusing
them, as opposed to being out of reach, they are given up after a few
attempts, and the client's whole state is written again, replacing what the
database holds for it, as soon as the database takes it.

The SQLite database runs in WAL mode: while the broker runs, it keeps
`-wal` and `-shm` files next to the database, and recent changes may be
only in the `-wal` file. To back it up while the broker runs, use
`sqlite3 mercurio.db ".backup copy.db"`; a plain copy of the database file
alone can miss those changes. The broker creates the database readable by
its owner only, and holds a lock on `mercurio.db.lock` while it runs, so a
second broker started on the same database exits with an error; reading
the database with `sqlite3`, as for a backup, still works. The database
records its schema version, and a broker refuses a database written by a
newer version of it.

Only one broker serves from a PostgreSQL database at a time. Another one
started on it exits, or, with `standby = true`, waits without listening and
takes over as soon as the serving broker's process stops, or about 11
seconds after its host goes down. A broker that can't reach the database for
about 7 seconds stops and exits with an error, as a standby may take over;
the database refuses writes from a broker that was taken over. PostgreSQL
needs version 12 or later, connected to directly, without PgBouncer.

### 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`, `subscriptions/count` and
`publish/messages/dropped`, the messages lost before reaching a subscriber.
Mercurio adds the dropped messages by cause under
`publish/messages/dropped/` (`queue_full_qos0`, `queue_full`, `expired`,
`too_large`, `not_authorized`, `no_share_member`) and, under `storage/`,
the batches of session changes written again after a storage error
(`storage/batches/retried`), those given up on since the broker started
(`storage/batches/dropped`) and the clients whose stored state is to be
written whole again because of them (`storage/clients/dirty`). Under
`load/` come the 1, 5 and 15
minute averages per minute of messages, bytes and connections, such as
`load/messages/received/1min`. Clients
may subscribe to them, but not publish to `$SYS` topics. Retained `$SYS`
messages, bridge states included, are kept in memory: new subscribers get
the latest values, but they are not stored and don't count toward
`max_retained_messages`.

A WebSocket listener with `health = true` answers `GET /healthz` with 200
while the broker serves; with `metrics = true`, `GET /metrics` gets the same
counters in the Prometheus text format, as `mercurio_*` metrics. Both are
off by default. Metrics show anyone who reaches the listener how loaded the
broker is, so serve them on a listener of their own on a loopback address,
as in the sample above.

## Running

### Direct execution

```bash
# With the default config location
mercuriod

# With a custom config file
mercuriod -c /path/to/config.toml

# Override the listen address
mercuriod -l 0.0.0.0:1883

# Debug logging
mercuriod -v
```

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`](contrib/mercuriod.service).
It runs the broker sandboxed, with `/var/lib/mercurio` (for the SQLite
database) and `/var/log/mercurio` (for a log `file`) as its only writable
directories. Files the broker creates are readable by its user only, and
it may keep 65536 files open, as each client connection takes one.

The unit is `Type=notify`: the broker sends `READY=1` to `$NOTIFY_SOCKET`
once its listeners are bound and it serves, so `systemctl start` returns,
and units ordered after it start, only then. A PostgreSQL standby sends it
once it takes over, and until then reports `STATUS=Standing by: ...` in
`systemctl status`; as that may take any time, the unit sets
`TimeoutStartSec=infinity`. On shutdown the broker sends `STOPPING=1`.

```bash
# Copy service file
sudo cp mercuriod/contrib/mercuriod.service /etc/systemd/system/

# Create config directory and copy config
sudo mkdir -p /etc/mercurio
sudo cp mercuriod/contrib/config.toml /etc/mercurio/

# Copy binary
sudo cp target/release/mercuriod /usr/bin/

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable --now mercuriod

# Check status
sudo systemctl status mercuriod

# View logs
journalctl -u mercuriod -f
```

## Features

See the [project README](../README.md#broker-mercuriod) for the full list
of supported MQTT features.