flashmq.conf(5)

πŸ“¦
version 1.27.3 Β· 2 versions
, in nixpkgs unstable

🌸 NAME 🌸

flashmq.conf β€” the configuration file format for FlashMQ, the MQTT broker. This is the flashmq.conf(5) manual page, bestie πŸ’–.

✨ SYNOPSIS ✨

The flashmq.conf file is the configuration used for configuring the FlashMQ MQTT broker.

πŸŽ€ CONFIG LOCATION πŸŽ€

By default, the flashmq daemon expects to find its configuration file at /etc/flashmq/flashmq.conf, but this can be overridden using the --config-file command-line argument; see the flashmq(1) man page for details.

Using the include_dir parameter in your config file, you can load all the *.conf files from that given directory. Easy peasy, slay. πŸ¦‹

πŸ¦‹ FILE FORMAT πŸ¦‹

Okay so to set a parameter, its name must appear on a single line, followed by one or more potentially quoted arguments. Like this:

parameter-name1 parameter-value
parameter-name2 'value with spaces'
parameter-name3 "escaped \" quote"
multi-value-param one 'two' "three" "with ' char"

Quoted values are the same as unquoted values when they don’t need it. They are necessary for when argument values have spaces, for instance. No cap.

When setting boolean values, yes/no, true/false and on/off can all be used.

To configure the listeners, use listen blocks, defined by { and }. See EXAMPLE LISTENERS for details.

Lines beginning with the hash character (β€œ#”) and empty lines are ignored. Thus, a line can be commented out by prepending a β€œ#” to it. Periodt. πŸ’…

πŸ‘‘ GLOBAL PARAMETERS πŸ‘‘

Here’s the big list, bestie β€” every global parameter, no skips, no cap. 🫢

  • plugin /path/to/plugin.so β€” FlashMQ supports an ELF shared object (.so file) plugin interface to add functionality, authorization and authentication, because it’s hard to provide a declarative mechanism that works for everybody. See flashmq_plugin.h for the API and its documentation. It’s written in C++ for ease of passing FlashMQ internals without conversion to C, but you can basically just use a C++ compiler and program like it was C; the C++ constructs are simple.

FlashMQ will auto-detect which plugin interface you’re trying to load (Mosquitto version 2 or FlashMQ native). Keep in mind that each thread initializes the plugin, inline with multi-core programming (minimize shared data and interaction between threads). You could use static variables with thread synchronization if you really want to. And of course, any Mosquitto plugin that uses global and/or static variables instead of initializing memory in its init() method, will not be thread-safe and won’t work.

You can only have one plugin active, but you can combine it with mosquitto_password_file and mosquitto_acl_file. The password and ACL file take precedence, and on a β€˜deny’, will not ask the plugin. Important bestie behavior.

  • plugin_opt_* value β€” Options passed to the plugin init() function.
  • plugin_serialize_init true|false β€” There could be circumstances where the plugin code is mostly thread-safe, but not on initialization. Libmysqlclient for instance, needs a one-time initialization. To add to the confusion, Qt hides that away.

The plugin should preferably be written with proper synchronization like that, but as a last resort, you can use this to synchronize initialization.

Default value: false

  • plugin_serialize_auth_checks true|false β€” Like plugin_serialize_init, but then for all login and ACL checks.

This option may be dropped at some point, because it negates much of the multi-core design. One may as well run with only one thread then.

Default value: false

  • plugin_timer_period seconds β€” The FlashMQ auth plugin interface has an optional function that is called periodically this amount of seconds. This can be used to refresh state, commit data, etc.

Setting a value of 0 disables it. You can enable and disable this timer with a config reload.

See flashmq_plugin.h for details.

Default value: 60

  • log_file /path/to/flashmq.log β€” This configuration parameter sets the path to FlashMQ's log file. If you omit this option from the config file, the output will go to stdout.
  • log_level debug|info|notice|warning|error|none β€” Set the log level to specified level and above. That means notice will log notice, warning and error.

Use this setting over the deprecated log_debug and quiet. If you do have those directives, they override the log_level, for backwards compatibility reasons.

Default value: info.

  • log_debug true|false β€” Debug logging obviously creates a lot of log noise, so should only be done to diagnose problems.

Deprecated. Use log_level instead.

Default value: false

  • log_subscriptions true|false β€” Default value: false
  • log_publishes true|false β€” Both publishes and QoS actions related to publishes are logged at this level.

Default value: false

  • allow_unsafe_clientid_chars true|false β€” If you have topics with client IDs in it, people can possibly manipulate your ACL checking by saying their client ID is 'John+foobar'. Audit your security before you allow this. Do not just vibe-check this and move on.

Default value: false

  • allow_unsafe_username_chars true|false β€” If you have topics with usernames in it, people can possibly manipulate your ACL checking by saying their username is 'John+foobar'. Audit your security before you allow this.

Default value: false

  • max_string_length bytes β€” The max length of almost all strings encoded in MQTT packets. This includes topics, user properties, client IDs, etc. Some fields, like passwords, are excluded. Payloads are also excluded (as they are not strings).

The reason is to avoid abuse. The default should be enough for most deployments.

If the limit is tripped, the error in the log will clearly say so, and mention max_string_length.

Default value: 4096

  • max_packet_size bytes β€” MQTT packets have a maximum size of about 256 MB. This memory will (temporarily) be allocated upon arrival of such packets, so there may be cause to set it lower.

This option works in conjunction with client_max_write_buffer_size to limit memory use.

Default value: 268435461

  • client_max_write_buffer_size bytes β€” The client's write buffer is where packets are stored before the event loop has the chance to flush them out. Any time a client's connection is bad and bytes can't be flushed, this buffer fills. So, there's good reason to limit this to something sensible. A good indication is your average packet size (or better yet, a configured max_packet_size) multiplied by the amount of packets you want to be able to buffer.

Despite the name, this settings also control the read buffer's maximum size. Having a bigger read buffer may be necessary when you know your clients receive many packets.

Note that it's an approximate value and not a hard limit. Buffer sizes only grow by powers of two, and buffers are always allowed to grow to make place for ping packets. Additionally, upon arrival of large packets (up to max_packet_size bytes), room will be made up to twice their size. So, you may also want to reduce max_packet_size from the default.

Default value is 1048576 (1 MB)

  • client_initial_buffer_size bytes β€” The buffers for reading and writing, also for websockets when relevant, start out with a particular size and double when they need to grow. If you know your clients send bulks of a particular size, it helps to set this to match, to avoid constant memory reallocation. The default value is set conservatively, for scenarios with millions of clients.

After buffers have grown, they are eventually reset to their original size when possible.

Also see client_max_write_buffer_size and max_packet_size.

Value must be a power of two.

Default value: 1024

  • mosquitto_password_file /foo/bar/mosquitto_password_file β€” File with usernames and hashed+salted passwords as generated by Mosquitto's mosquitto_passwd.

Mosquitto up to version 1.6 uses the sha512 algorithm. Newer version use sha512-pbkdf2. Both are supported.

  • mosquitto_acl_file /foo/bar/mosquitto_acl_file β€” ACL (access control lists) for users, anonymous users and patterns expandable with %u (username) and %c (clientid). Format is Mosquitto's acl_file.
  • allow_anonymous true|false β€” This option can be overridden on a per-listener basis; see listener.allow_anonymous.

Default value: false

  • zero_byte_username_is_anonymous true|false β€” The proper way to signal an anonymous client is by setting the 'username present' flag in the CONNECT packet to 0, which in MQTT3 also demands the absence of a password. However, there are also clients out there that set the 'username present' flag to 1 and then give an empty username. This is an undesirable situation, because it means there are two ways to identify an anonymous client.

Anonymous clients are not authenticated against a loaded plugin when allow_anonymous is true. With this option enabled, that means users with empty string as usernames also aren't.

With this option disabled, clients connecting with an empty username will be reject with 'bad username or password' as MQTT error code.

The default is to be unambiguous, but this can be overridden with this option.

Default value: false

  • rlimit_nofile number β€” The general Linux default of 1024 can be overridden. Note: systemd blocks you from setting it, so it needs to be set on the unit. The default systemd unit file sets LimitNOFILE=infinity. You may also need to set sysctl -w fs.file-max=10000000

Default value: 1000000

  • expire_sessions_after_seconds seconds β€” Expire sessions after this time. Setting to 0 disables it and is (MQTT3) standard-compliant. But, existing sessions cause load on the server (because they cost memory and are still subscribers), so keeping sessions after any client that connects with a random ID doesn't make sense.

Default value: 1209600

  • quiet true|false β€” okay so this one tells FlashMQ not to log LOG_INFO and LOG_NOTICE. Super useful when you've got a lot of foot traffic, because otherwise the log gets filled with connect/disconnect notices and you're just sitting there like 😭.
🚨 Deprecated: use log_level instead.

Default value: false.

  • storage_dir /path/to/dir β€” the place where sessions, subscriptions, and retained messages live. Not specifying this will turn off persistence. periodt.
  • save_state_interval seconds β€” the interval at which the state is saved, if enabled with storage_dir. This setting is also applied on reload. Default: 3623.
  • persistence_data_to_save all|sessions_and_subscriptions|retained_messages|bridge_info β€” when a storage_dir is defined, this picks which info should be saved. The arguments are a list of one or more of those options, and you can negate them with !, like all !retained_messages. The order is relevant β€” if you end with all, it overrides the previous. The bridge_info option causes a small bit of meta data of the bridges to be saved; see the bridge option use_saved_clientid. The default is all, but as soon as this option is defined, it's cleared, and the desired list has to be composed. 🫢
  • max_qos_msg_pending_per_client number β€” sets the max amount of QoS packets that can be stored in a session. If this is exceeded, the packet is dropped. Changing this only takes effect for new clients (also when picking up existing sessions). This is largely due to it being part of the MQTT5 connection handshake and is supposed to be adhered to. Default: 512.
  • max_qos_bytes_pending_per_client bytes β€” sets the max amount of bytes of QoS packets that can be stored in a session. If this is exceeded, the packet is dropped. Default: 65536.
  • max_qos qos_value β€” the maximum QoS value FlashMQ will allow clients to use for subscriptions, publishes, and wills. Subscriptions will be downgraded to this value. Publishes and wills will cause a disconnect for MQTT5 clients, and the action configured with mqtt3_qos_exceed_action for MQTT3 clients. The value is updated on config reload, but only for new clients. The reason is that the max QoS is part of the handshake in MQTT5, and clients will be surprised (and cause disconnects) if that changes during the lifetime of a connection. For consistency, the MQTT3 clients behave the same. This setting is also available per listener. Default value: 2.
  • mqtt3_qos_exceed_action drop|disconnect β€” unlike MQTT5, MQTT3 doesn't have defined behavior for exceeding a configured maximum QoS (see max_qos). This allows the behavior to be controlled. In case of drop, the client gets an acknowledgement as if the publish has succeeded, to avoid stale in-flight packets. This setting is also available per listener. Default value: disconnect.
  • max_incoming_topic_alias_value number β€” is communicated towards MQTT5 clients. It is then up to them to decide to set them or not. Changing this setting and reloading the config only has effect on new clients, because existing clients would otherwise exceed the limit they think applies. Default value: 65535.
  • max_outgoing_topic_alias_value number β€” FlashMQ will make this many aliases per MQTT5 client, if they ask for aliases (with the connect property TopicAliasMaximum). Default value: 65535.
  • thread_count number β€” if you want to have a different amount of worker threads than CPUs, you can set this value. Typically you don't need to set this. Default value: auto-detect.
  • wills_enabled true|false β€” when disabled, the server will not set last will and testament specified by connecting clients. Default value: true.
  • retained_messages_mode enabled|enabled_without_persistence|downgrade|drop|disconnect_with_error β€” retained messages can be a strain on the server that you may not need, so here are the ways of dealing with them:
    • enabled β€” this is normal operation.
    • enabled_without_persistence β€” like normal, except it won't store them to disk if storage_dir is defined.
    • enabled_without_retaining β€” this somewhat counter-intuitive sounding mode is like downgrade, except the retain flag is not removed. This allows MQTT5 subscribers that subscribe with retain as published to see which messages were originally sent as retained. It's just that FlashMQ won't retain them.
    • downgrade β€” the retain flag is removed and treated like a normal publish.
    • drop β€” messages with retain set are dropped.
    • disconnect_with_error β€” disconnect clients who try to set them. Default value: enabled.
  • expire_retained_messages_after_seconds seconds β€” use this to limit the life time of retained messages. Without this, the amount of retained messages may never decrease. Default value: 4294967295.
  • retained_messages_delivery_limit number β€” deprecated. πŸ’€
  • retained_messages_node_limit number β€” when clients place a subscription, they will get the retained messages matching that subscription. Even though traversing the retained message tree is deprioritized in favor of other traffic, it will still cause CPU load until it's done. If you have a tree with millions of nodes and clients subscribe to #, this is potentially unwanted. You can use this setting to limit how many nodes of the retained tree are traversed. Note that the topic one/two/three is three nodes, and each node doesn't necessarily need to contain a message. Default value: 4294967295.
  • retained_messages_node_creation_limit_enforcement_mode log|reject β€” sets the enforcement mode for the retained-message node creation limit. Values are log and reject.
  • retained_messages_node_creation_limit_enforcement_depth number β€” the depth in the tree at which enforcement starts. Default is 0, meaning the root of the tree, so that the limit is a global limit. Using a value higher than 0 makes sense when combining it with a permission system. For instance, if your topics are like dev123/temperature, dev456/temperature, you can set it to 1, so that a client is limited to their own topic tree, and can't block other clients from setting messages by flooding the server.
  • retained_messages_node_creation_limit number β€” to prevent excessive resource use by (misbehaving) clients, you can limit the amount of retained message nodes that can be created. Note that the topic one/two/three is three nodes, and each node doesn't necessarily need to contain a message. A rejected message is still delivered to on-line clients; it's just not set as retained message. When the limit is reached, a unique message per enforcement node is logged. Clients don't get to see that their publish message did not result in a retained message. There are various reasons for that: QoS 0 messages don't have ACKs, MQTT3 doesn't even have the ability to report a deny for QoS messages, and neither MQTT3 nor MQTT5 have the ability to report the message was accepted, just not for retained. Furthermore, there are various asynchronous or background ways to set retained messages, like last will and testament. Defaults:
    • retained_messages_node_creation_limit_enforcement_mode: reject
    • retained_messages_node_creation_limit_enforcement_depth: 65535 (effectively disabled)
    • retained_messages_node_creation_limit: unlimited
  • retained_message_max_payload_size bytes β€” to prevent excessive resource use by (misbehaving) clients, you can set a maximum payload size of a retained message. When the limit is exceeded, a unique message per node is logged. Clients don't get to see that their message was rejected. There are various reasons for that, as outlined at retained_messages_node_creation_limit. Default value: unlimited (max packet size).
  • set_retained_message_defer_timeout milliseconds β€” the time after which FlashMQ will fall back to (b)locking vs queued mode for setting retained messages. 0, the default, disables queued mode altogether. It's disabled by default because it can incur some extra CPU and memory overhead. Each retained message lives in a node in a tree. The topic one/two/three is three nodes. When a node in that tree does not exist yet, it needs to be created. This requires a write lock on the tree. At this point, other threads reading from or writing to the retained message tree need to wait. This can cause a compounding blocking effect, especially if many threads do it at once. This feature is to favor server responsiveness vs the speed at which retained messages become available in the server. It is primarily useful for when you have a lot of retained messages on different/changing topics. If at first a retained message can't be set, the action to do so will be retried in the event loop, asynchronously. This setting determines the maximum amount of time to defer setting a retained message, after which it will fall back to using locks. Also see set_retained_message_defer_timeout_spread. Default value: 0.
  • set_retained_message_defer_timeout_spread milliseconds β€” for set_retained_message_defer_timeout, the amount of random spread between 0 and this value for the timeout. This spreads out locking over time, reducing contention. Default value: 1000.
  • retained_message_node_lifetime seconds β€” the grace period after which a retained message node is eligible for deletion. The topic one/two/three is three nodes, and if that topic had a message, it would be contained in three.

FlashMQ will periodically clear out retained message nodes that have no message anymore β€” it literally has to, that's how the memory stays cute and not huge πŸ’–. But like, when you receive retained messages on the same topics over and over, it can lowkey be worth keeping those nodes around, because then it doesn't have to grab locks to recreate them. So if you know retained messages come and go within a certain window, setting this value is genuinely beneficial: no unnecessary node destruction and creation happening in the background πŸŽ€.

Default value: 0


✨ subscription_node_lifetime seconds ✨

The grace period before a subscription node becomes eligible for deletion, bestie. And yes, the subscription one/two/three counts as three nodes πŸ‘‘

FlashMQ periodically clears out nodes in the subscription tree that have no entries anymore β€” required for memory reasons, no cap. But when clients place the same subscriptions repeatedly, keeping the nodes around can be beneficial, because it avoids the locks needed to recreate them. If you know certain subscription patterns come and go within a certain period, set this value so no unnecessary node destruction and creation is happening. Obsessed with a tidy tree 🌸

Default value: 3600


πŸ¦‹ websocket_set_real_ip_from inet4_address|inet6_address πŸ¦‹

Okay so HTTP proxies sitting in front of your websocket listeners can set the X-Real-IP header to identify the original connecting client. With websocket_set_real_ip_from you mark IP networks as trusted. By default, clients are not trusted β€” anti-spoofing era, we love to see it 🫢

You can repeat the option to allow multiple addresses. Valid notations are 1.2.3.4, 1.2.3.4/16, 1.2.0.0/16, 2a01:1337::1, 2a01:1337::1/64, etc.

The header X-Forwarded-For is not used, because that one's designed to contain a list of addresses, if applicable. Different vibe, not our vibe πŸ’…

Side note about using a proxy on your listener: you can only have an absolute max of 65535 connections to an IP+port combination (and the practical limit is lower than that 😭). If you need more, you have to set up multiple listeners. That can be multiple IP addresses, or just simply multiple ports.

πŸ”₯ shared_subscription_targeting round_robin|sender_hash|first πŸ”₯

When you have multiple subscribers on a shared subscription (like '$share/myshare/jane/doe'), this picks how the messages get distributed over those subscribers ✨

  • round_robin β€” select the next subscriber for each message. There's still some amount of randomness in there, because the counter for this is not thread safe. Using an atomic/mutexed counter for it would just be too slow to justify, and honestly? Fair πŸ’€
  • sender_hash β€” selects a receiver deterministically based on the hash of the client ID of the sender. The selected subscriber depends on how many subscribers there are, so if some disconnect, the distribution changes. Also, the selection may change when FlashMQ cleans up empty spaces in the list of shared subscribers. Keep that in mind, iykyk.
  • first β€” selects the first subscriber in the list. This mode can be useful for fallback: when one client disappears, the other one will seamlessly take over. Main character energy, lowkey πŸ‘‘

Default: round_robin


🧚 minimum_wildcard_subscription_depth number 🧚

Defines the minimum level of the first wildcard topic filter (# and +). In a topic filter like sensors/temperature/#, that's 2. So if you specify 2, a subscription to sensors/# gets denied. Remember though β€” only MQTT 3.1.1 and newer actually notify the client of the denial in the sub-ack packet. Older ones just get silently denied, sorry bestie πŸ™ˆ

Why would you even want to limit it? Performance, babe. If you have a base message load of 100,000 messages per second, every client subscribing to # causes that many permission checks per second. And if you have 100 clients doing that... that's 10 million permission checks per second. Absolutely not 😭

Default: 0


πŸ’œ max_topic_split_depth number πŸ’œ

Defines the maximum number of components a topic/filter string can have. For example, one/two/three has three.

The reason? Avoiding abuse, periodt. The default should be enough for most deployments.

If the limit is tripped, the error in the log will clearly say so, and it will mention max_topic_split_depth β€” so you'll know exactly who did it πŸŽ€

Default: 128


🌈 wildcard_subscription_deny_mode deny_all|deny_retained_only 🌈

For minimum_wildcard_subscription_depth, this specifies what you actually want to deny. Trying to give a client all the retained messages can cause quite some load, so only denying the retained messages when receiving a broad wildcard subscription can be useful β€” that's the play if you have a low enough general message volume but a high number of retained messages ✨

Default: deny_all


πŸ’… overload_mode log|close_new_clients πŸ’…

Defines the action to perform when the value set with max_event_loop_drift gets exceeded.

Here's the situation: when a server is (re)started and hundreds of thousands of clients connect, the SSL handshaking and authenticating can be so heavy that it doesn't get to clients in time. Then they reconnect and try again, and get stuck in a loop 😭 this option is the mitigation. With close_new_clients, new clients get closed immediately after connecting while the server is overloaded. That lets the worker threads process new clients in a controlled manner. Controlled chaos only πŸ’–

For really large deployments, you can augment this with extra rate limiting in iptables, or other firewalls. A stateless method is preferred, like:

iptables -I INPUT -p tcp -m multiport --dports 8883,1883 --syn -m hashlimit --hashlimit-name newmqttconns --hashlimit-above 10000/second --hashlimit-burst 15000 -j DROP

The current default is log, but that will likely change in the future πŸ‘€

Default: log


⭐ max_event_loop_drift milliseconds ⭐

For overload_mode, this is the maximum permissible thread drift before the overload action gets taken.

The drift values considered are those of the main loop (the one where clients are accepted) and the median of all worker threads.

Default: 2000


πŸŽ€ include_dir /path/to/dir πŸŽ€

Load *.conf files from the specified directory, to merge them with the main configuration file. So organised, so slay 🌸

An error is generated when the directory isn't there. That's on purpose β€” it protects you from accidentally running an incorrect config, like when the dir has been renamed, for example. Safety first, bestie 🫢


πŸ‘‘ subscription_identifiers_enabled true|false πŸ‘‘

Subscription identifiers let clients see which subscription was responsible for a message. Publish messages will contain the identifier that was included in the original subscription. Detective work, but make it MQTT πŸ”

Heads up though: enabling this prevents FlashMQ from using optimizations involving packet reuse, because packets are unique per client once they contain a subscription identifier. So you may want to assess the performance difference in high message volume deployments before you commit, ngl.

As per the spec, clients sending subscription identifiers when the server reported the feature as unavailable will cause them to be disconnected. Side effect incoming: changing this setting on a running server will disconnect clients when they send a subscription with an identifier in it. This was chosen over the alternatives because of simplicity and operator control β€” otherwise it literally can't be turned off at all for existing clients. Yeah. That's the trade 😭

Default value: true

✨ LISTEN PARAMETERS ✨

okay so these ones are special πŸ’… β€” they can only live inside listen { } blocks. you don't just sprinkle them anywhere, bestie.

  • port β€” the default value here is a whole vibe check of its own, because it depends on the protocol parameter and on whether or not you supplied fullchain and privkey:
    • for unencrypted MQTT, the default port is 1883
    • for encrypted MQTT, the default port is 8883
    • for plain HTTP websockets, the default port is 8080
    • for encrypted HTTPS websockets, the default port is 4443
  • protocol mqtt|websockets|acme β€” this one is a required parameter, no skipping it 🫢. for acme, go peek at acme_redirect_url.
  • inet_protocol ip4_ip6|ip4|ip6|unix β€” when you go with unix, a unix_socket_path is required. Default: ip4_ip6
  • inet4_bind_address inet4address β€” Default: 0.0.0.0
  • inet6_bind_address inet6address β€” Default: ::0
  • unix_socket_path path β€” when you're using unix for inet_protocol, this is the file path of the socket. and listen, FlashMQ will straight up remove pre-existing socket files if they already exist πŸ’€
  • unix_socket_user user β€” use this to set the owner of the socket. it will always be attempted, but a warning may get logged if it doesn't work out 😭. users may be specified as numeric or names. also β€” a config test will not verify the existence of users, for portability. so like, don't come for it.
  • unix_socket_group group β€” use this to set the group of the socket. again, it will always be attempted, but a warning may be logged if it's not successful. groups may be specified as numeric or names. and same deal: a config test will not verify the existence of groups, for portability ✨
  • unix_socket_mode mode β€” use this to specify the permission mode of the unix socket, like 600.
  • fullchain /foobar/server.crt β€” specifying a chain makes the listener SSL, and it also requires the privkey to be set. they're a package deal πŸŽ€
  • privkey /foobar/server.key β€” specifying a private key makes the listener SSL, and it also requires the fullchain to be set. same energy, reverse direction.
  • minimum_tls_version tlsv1.1|tlsv1.2|tlsv1.3 β€” sets the minimum supported TLS version for TLS listeners. heads up though: setting this value low may not actually enable that protocol version if OpenSSL won't support it (anymore) πŸ™ˆ the TLS version clients use gets logged πŸ“ Default: tlsv1.1
  • client_verification_ca_file /foobar/client_authority.crt β€” clients can be authenticated using X509 certificates, and the username gets taken from the CN (common name) field. use this directive to specify the certificate authority you trust 🫢 specifying this or client_verification_ca_dir will require the listener to be TLS.
  • client_verification_ca_dir /foobar/dir_with_certificates β€” clients can be authenticated using X509 certificates, and the username gets taken from the CN (common name) field. use this one to specify the dir containing certificate authorities you trust. note that the filename requirements are dictated by OpenSSL β€” use the utility openssl rehash /path/to/dir. specifying this or client_verification_ca_file will require the listener to be TLS.
  • client_verification_still_do_authn true|false β€” when you're using X509 client authentication with client_verification_ca_file or client_verification_ca_dir, the username will not be checked with a user database or a plugin by default. set this option to true to override that πŸ’…
  • acme_redirect_url http://example.com/ β€” this allows an ACME (automated certificate management environment) challenge to be redirected elsewhere. it lets you decouple the certificate creation from the host(s) that run FlashMQ, which is lowkey iconic ✨ this can either be configured on a dedicated listener with protocol acme, or multiplexed on a non-SSL mqtt or websockets listener.
  • drop_on_absent_certificate true|false β€” when both privkey and fullchain are absent, don't create this listener. this helps in situations where you don't have the certificate and key yet, but you're expecting them πŸ‘€
  • only_allow_from inet4_address|inet6_address β€” when set, restricts the listener to the source address/network given. you can repeat the option to allow multiple addresses/networks πŸŽ€ valid notations are 1.2.3.4, 1.2.3.4/16, 1.2.0.0/16, 2a01:1337::1, 2a01:1337::1/64, etc.
  • deny_from inet4_address|inet6_address β€” block connections from this address or network 🚫 you can repeat the option multiple times. valid notations are 1.2.3.4, 1.2.3.4/16, 1.2.0.0/16, 2a01:1337::1, 2a01:1337::1/64, etc.
  • allow_anonymous true|false β€” this lets you override the global allow_anonymous setting, but like, at the listener level πŸ’…
  • overload_mode log|close_new_clients β€” same deal, this lets you override the global overload_mode setting on the listener level.
  • haproxy off|on|client_verification|client_verification_with_authn β€” okay so the whole nature of running something behind a proxy is that you'd lose certain information or abilities 😭 and for that, HAProxy allows sending information in a header frame. with the haproxy option you can configure the following:
    • off and on configure basic functionality: reading the client's source address from the haproxy frame. this address will be shown in the logs and plugin functions 🫢
    • client_verification and client_verification_with_authn will require haproxy to send the PP2_SUBTYPE_SSL_CN field when using mTLS (mutual TLS), i.e. client authentication. when using the client_verification_with_authn option, FlashMQ's internal authentication will still happen (with the username from the certificate). this can be convenient when you have a plugin, so that you can still take action in the login hook ✨
make sure this listener is private, firewalled or use only_allow_from β€” otherwise anybody can set a different source address πŸ’€ fr fr.

note that HAProxy's server health checks only started using the local specifier as of version 2.4. this means earlier version will pretend to be a client and break the connection, causing log spam 😭

as a side note about using a proxy on your listener: you can only have an absolute max of 65535 connections to an IP+port combination (and the practical limit is lower). if you need more, you have to set up multiple listeners. this can be multiple IP addresses, or simply multiple ports. no cap. πŸ¦‹

the following is an example HAProxy backend config for the features described:

backend backend_server
        mode tcp
        timeout client  2m
        timeout connect 10s
        timeout server  2m
        server server1 ::1:1885 send-proxy-v2 send-proxy-v2-ssl-cn

see haproxy.org [http://www.haproxy.org/].

  • tcp_nodelay true|false β€” tcp_nodelay will cause the TCP_NODELAY option to be set for the listener's socket(s), and therefore for all clients accepted on that listener. TCP_NODELAY is an OS TCP-layer option that will cause messages written by FlashMQ to the socket to be flushed immediately, without letting Nagle's algorithm (the default) collect small outgoing TCP packets into bigger packets πŸŽ€ foregoing Nagle's algorithm by setting tcp_nodelay to true may decrease latency, at the likely cost of some network efficiency βš–οΈ Default: false
  • max_buffer_size number β€” override the client_max_write_buffer_size for this listener. this is especially useful when this listener is the receiving side of a bridge, because these clients will likely see more traffic πŸ”₯
  • max_qos qos_value β€” per-listener counterpart of max_qos ✨
  • mqtt3_qos_exceed_action drop|disconnect β€” per-listener counterpart of mqtt3_qos_exceed_action.

πŸŽ€ EXAMPLE LISTENERS πŸŽ€

like, just copy the vibe bestie:

listen {
  protocol mqtt
  inet_protocol ip4_ip6
  inet4_bind_address 127.0.0.1
  inet6_bind_address ::1
  fullchain /foobar/server.crt
  privkey /foobar/server.key
  # default = 8883
  port 8883
}
listen {
  protocol mqtt
  fullchain /foobar/server.crt
  privkey /foobar/server.key
  client_verification_ca_file /foobar/client_authority.crt
  client_verification_still_do_authn false
}
listen {
  protocol mqtt
  inet_protocol ip4
  # default = 1883
  port 1883
}
listen {
  protocol websockets
  fullchain /foobar/server.crt
  privkey /foobar/server.key
  # default = 4443
  port 4443
}
listen {
  protocol websockets
  # default = 8080
  port 8080
}
listen {
  port 2883
  haproxy on
}

πŸŒ‰ BRIDGE CONFIGURATION πŸŒ‰

bridges can be defined inside bridge { } blocks. a bridge is essentially just an outgoing connection to another server with loop-detection and retain flag relaying 🫢 it is not a form of clustering, although with careful design it can be deployed to achieve some sort of load balancing.

okay so important caveat time πŸ‘‘: normally (unless connection_count is set) one bridge is one connection, and because FlashMQ's threading model is that clients are serviced by one selected thread only, a bridge has the potential to saturate a thread if it's heavily loaded 😭 you can improve that with connection_count.

bridges are dynamically created, removed or changed upon config reload ✨ when a bridge configuration changes, it will disconnect and reconnect. it's giving main character energy, honestly.

  • address address β€” the DNS name, IPv4 or IPv6 address of the server you want to connect to.
  • port number β€” the default port depends on the tls option, either 1883 or 8883.
  • inet_protocol ip4_ip6/ip4/ip6 β€” Default: ip4_ip6
  • tls off/on/unverified β€” set TLS mode. the value unverified means the x509 chain is not verified πŸ™ˆ
  • minimum_tls_version tlsv1.1|tlsv1.2|tlsv1.3 β€” set the minimum supported TLS version the bridge will negotiate with the other side. note that setting this value low may not actually enable that protocol version if OpenSSL won't support it (anymore). Default: tlsv1.1

  • fullchain /foobar/bridge.crt β€” with TLS enabled, dropping a chain here makes the bridge connection authenticate itself to the remote broker with a public certificate. Heads up though: this one also requires privkey to be set too. They're a package deal, no solo acts. πŸ”
  • privkey /foobar/bridge.key β€” the other half of that package deal. With TLS enabled, giving a private key makes the bridge connection to the remote broker use that key β€” and it requires fullchain to be set right back. πŸ’–
  • ca_file path β€” the file you point at for x509 certificate chain validation.
  • ca_dir path β€” a whole directory of certificates for x509 certificate chain validation. Yes, a whole directory, she's thorough. ✨
  • protocol_version mqtt3.1|mqtt3.1.1|mqtt5 β€” which MQTT flavour this bridge speaks. Pick from mqtt3.1, mqtt3.1.1 or mqtt5. Default: mqtt3.1.1
  • bridge_protocol_bit true|false β€” okay so there's this unofficial standard where you set the most significant bit of the protocol version byte to 1 to signal "hey, this connection is a bridge." It lets the other side tweak its behaviour a lil bit. BUT β€” and this is the important part β€” it's not always supported, so if you keep getting disconnected over reporting an invalid protocol version, you can turn this off. πŸ™ˆ Also: this setting does nothing when you're using MQTT5, because the behaviour it influences is handled by subscription options instead. Default: true
  • keepalive seconds β€” how long between sending ping packets to the other side. Default: 60
  • clientid_prefix prefix β€” the prefix for the randomly generated client ID. Ngl, you cannot set client IDs explicitly, for security reasons β€” that's just how it is. See "Understanding clean session and clean start" over at https://www.flashmq.org/2022/11/26/understanding-clean-session-and-clean-start/. Default: fmqbridge
  • publish filter qos β€” messages matching this filter get published over to the other side. Examples: # or sport/tennis/#. You can repeat this option as many times as you want. πŸ’… About that QoS value: think of it as the QoS of the internal subscription that causes the outgoing messages. Messages that get relayed carry this QoS level at most. Default: 0
  • subscribe filter qos β€” a subscription for this filter gets placed on the other side. Examples: # or sport/tennis/#. Repeatable, same as above. πŸ”₯ The QoS value works like any subscription at a server would β€” messages received by the other end get given this QoS level at most. Default: 0
  • local_username username β€” the username as seen by the local FlashMQ's plugin or ACL checks. Not always necessary, so don't stress if you skip it.
  • remote_username username β€” the username sent to the remote connection.
  • remote_password password β€” the password sent to the remote connection. 🀫
  • remote_clean_start true|false β€” buckle up, this one's a whole thing. πŸŽ€
    • In MQTT3, this means 'clean session'. Which means: on (re)connect, the remote server removes any existing session with the same ID, and destroys it immediately on disconnect. Want reusable sessions that survive disconnects? Set this to false. And if you also want to pick up remote sessions when FlashMQ restarts, set use_saved_clientid to true.
    • In MQTT5, this option only influences reconnection behaviour. It essentially has no effect on the very first connect, because the client ID is random and will always be new (unless you set use_saved_clientid). But when it's set to true, any reconnects β€” which do use the already generated client ID β€” will destroy the session, and in-flight messages will be lost. 😭
    • Also see understanding clean session and clean start at https://www.flashmq.org/2022/11/26/understanding-clean-session-and-clean-start/. Default value: true
  • local_clean_start true|false β€” the local-side twin of the above.
    • In MQTT3 mode this means 'clean session', i.e. the session gets removed upon disconnect. Want to reuse sessions on reconnect? Set this to false. Any new start of FlashMQ gives you a new client ID, so it'll always be a fresh session β€” except if you set use_saved_clientid. πŸ¦‹
    • In MQTT5 mode this only has effect on start, where any existing local session is removed if found. Want the session removed immediately on disconnect instead? Set local_session_expiry_interval to 0.
    • Also see understanding clean session and clean start at https://www.flashmq.org/2022/11/26/understanding-clean-session-and-clean-start/. Default value: true
  • remote_session_expiry_interval seconds β€” MQTT5 mode only, bestie. Says how many seconds it takes before the session can be removed from the remote server. Default value: 0
  • local_session_expiry_interval seconds β€” determines when a local session with no active client gets removed, in both MQTT3 and MQTT5 mode. Small note: in MQTT3 mode, the session is removed on disconnect when local_clean_start is true. Default value: 0
  • remote_retain_available true|false β€” MQTT5 lets a server tell a client it doesn't support retained messages, or that it has them disabled. If you're on MQTT3, use this option to achieve the same vibe. 🌈 Result: messages won't be relayed with 'retained as published', and the retained messages that normally get sent on a matching subscription are not sent. Default value: true
  • use_saved_clientid true|false β€” so you want your bridges to resume local and remote sessions after a restart? Set this to true, and set remote_clean_start, local_clean_start, remote_session_expiry_interval and local_session_expiry_interval accordingly. ⭐ It only does anything when you've set a storage_dir and included bridge_info in persistence_data_to_save. Now listen, because this matters: it's important to fully understand clean session / clean start behaviour and the role the client ID plays in it. The primary goal of sessions is to survive link disconnects. Configuring a fixed client ID and using it every time an MQTT client starts is often an anti-pattern β€” because most clients (like actual IoT devices) start fresh on restart and don't store their sessions (with in-flight packets, etc.) to disk. FlashMQ does store it on disk however, so it can be used legitimately. 🫢 But you can run into unexpected situations. For instance: you'll get your existing subscriptions from the session too. So if you remove a subscribe line from your bridge config and restart, it will actually have no effect, because the server on the other side still has that subscription in the session. πŸ’€ See understanding clean session and clean start at https://www.flashmq.org/2022/11/26/understanding-clean-session-and-clean-start/ for the details. Default value: false
  • max_outgoing_topic_aliases amount β€” if you want FlashMQ to initiate topic aliases for this bridge, set this to a non-zero value. Warning though: it gets floored to whatever value the remote side gives in the CONNACK packet, so it only works if the other side permits it. 🀝 Default: 0
  • max_incoming_topic_aliases amount β€” if you want to accept topic aliases for this bridge, set this to a non-zero value. The value goes in the CONNECT packet to tell the remote side what you're wishing for. No cap, it's not guaranteed the other side will actually make aliases. Default: 0
  • tcp_nodelay true|false β€” makes the TCP_NODELAY option get set for the client socket used to connect to the other end of the bridge. See the documentation for the tcp_nodelay listener parameter for further elaboration. Default: false
  • local_prefix prefix β€” prefixes remap topics to and from the other end of the bridge. Which is iconic, because it makes it possible to insert a topic tree into the topic tree on another server, like a shared one. 🌸 Here's the flow, bestie:
    • When a message comes in, remote_prefix is stripped from the topic and local_prefix is added. That resulting topic is what gets used for authorization 'write' checking.
    • When a message goes out, the opposite happens: local_prefix is stripped and remote_prefix is added. But this time the original topic is used for authorization 'read' checking.
    • The prefixes are not applied to the subscribe and publish bridge options. You'll have to include the prefix in the subscriptions you configure yourself. That's on purpose β€” so you can have multiple subscriptions to the other end and only have the prefix applied to the relevant one(s). Messages that come in and go out that don't match the prefixes are sent and received unchanged. πŸŽ€
    • Prefix removal is not done to topics that match the prefix exactly. Why? To avoid one/two/three/ (which has a legal empty string as its last subtopic) becoming an empty string (which is illegal). πŸ™…
    • If you define a prefix, it's required to end with a /. It's totally valid to have only a local prefix or only a remote prefix.
  • remote_prefix prefix β€” see local_prefix above. ^^
  • connection_count number|auto β€” normally a bridge has one TCP connection to the other side. Which means dealing with bridge traffic is limited to one thread β€” and that also applies to the remote side if it's also FlashMQ. With this option you can give a bridge multiple connections and share the traffic load over them using MQTT5 'shared subscriptions'. πŸ‘‘ So when you specify a publish or subscribe path of one/two/three, the topic gets adjusted to $share/RANDOM/one/two/three to create a shared subscription, so the load gets balanced. With FlashMQ, load balancing is especially important for the side processing publishes β€” because every received publish packet means subscribers have to be looked up, auth checked, etc. πŸ’… The shared_subscription_targeting mode is automatically set to sender_hash. That's required to make sure you get sequential message relaying (so ordering is retained), and it's also better in plugin code when messages from one source are kept to one thread β€” which is how it'd be under normal circumstances.

If FlashMQ is also on the receiving end of those load-balanced connections, it's got a little extra trick up its sleeve: it groups the clients of one bridge together so loop detection actually works. Normally MQTT5 hands you the 'no-local' subscription option for exactly this vibe, but the standard says nope, that's not allowed for shared subscriptions. So FlashMQ uses 'user properties' to gossip about which group everyone belongs to, and boom β€” we can still do that loop detection. ✨

This means you can go ahead and specify overlapping paths in the subscribe and publish options of a bridge. Lowkey important though: both ends need to be running FlashMQ version 1.22.0 or higher. 🫢

And when the target server really is also FlashMQ, it's highkey smart to spin up a dedicated listener, for a few reasons:

  • Incoming connections get handed to threads in sequential order per listener β€” which gives you the best spread over your worker threads. πŸ‘‘
  • It also lets you set overload_mode, max_buffer_size, and possibly only_allow_from and deny_from differently just for that listener. πŸŽ€

For connection_count, you can give it an actual number of connections, or auto for one connection per CPU. FlashMQ's load is mostly on the receiver of messages, so auto will likely be your bestie if most of your message load from the other side is incoming rather than outgoing. Otherwise, match it up to the other side's number of CPUs. 🧚

Default: 1
  • max_buffer_size number β€” overrides the client_max_write_buffer_size for this bridge. And when connection_count is in play, this size applies to each connection individually. πŸ’… Bridges typically carry way more traffic than single clients β€” so yeah, it makes sense to bump this up. πŸ”₯

🌈 EXAMPLE BRIDGE 🌈

Okay bestie, here's the whole thing in action:

bridge {
    address demo.flashmq.org
    publish send/this
    subscribe receive/this
    local_username my_local_user
    remote_username my_remote_user
    remote_password my_remote_pass
    bridge_protocol_bit false
    tls on
    ca_file /path/to/ca.crt
}

Iconic. Ate. No notes. πŸ’–


πŸ‘‘ AUTHOR πŸ‘‘

Wiebe Cazemena β€” jk, it's Wiebe Cazemier <contact@flashmq.org>. 🫢


πŸ”­ SEE ALSO πŸ”­

Go read 'em, iykyk. πŸ’œ


πŸŽ€ COLOPHON πŸŽ€

The sources for the FlashMQ manual pages live in DocBook 5.2 [https://tdg.docbook.org/tdg/5.2/] XML files. Turning them into the multiple destination file formats is done with a whole bunch of XSLT 1.0 sheets, contributed to this project by Rowan van der Molen. Literally so generous of them. 🌸

The groff source of this man-page has ANSI-color support for the terminal 😭✨ β€” except Debian-derived Linux distributions turn off groff color support by default. Rude. To override that, just set the GROFF_SGR environment variable to 1. Periodt. πŸ’…

That's a wrap, bestie πŸ¦‹ β€” this page is dated jun 20 2026, from flashmq.conf(5), and yes we made it all the way to the end. Slay. 🌟
πŸ“¦ where this page comes from