···
cfg = config.services.mosquitto;
+
# note that mosquitto config parsing is very simplistic as of may 2021.
+
# often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
+
# there's no escaping available either, so we have to prevent any being necessary.
+
str = types.strMatching "[^\r\n]*" // {
+
description = "single-line string";
+
path = types.addCheck types.path (p: str.check "${p}");
+
configKey = types.strMatching "[^\r\n\t ]+";
+
optionType = with types; oneOf [ str path bool int ] // {
+
description = "string, path, bool, or integer";
+
if isBool v then boolToString v
+
else if path.check v then "${v}"
+
assertKeysValid = prefix: valid: config:
+
assertion = valid ? ${n};
+
message = "Invalid config key ${prefix}.${n}.";
+
formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
+
userOptions = with types; submodule {
+
type = uniq (nullOr str);
+
Specifies the (clear text) password for the MQTT User.
+
passwordFile = mkOption {
+
type = uniq (nullOr types.path);
+
example = "/path/to/file";
+
Specifies the path to a file containing the
+
clear text password for the MQTT user.
+
hashedPassword = mkOption {
+
type = uniq (nullOr str);
+
Specifies the hashed password for the MQTT User.
+
To generate hashed password install <literal>mosquitto</literal>
+
package and use <literal>mosquitto_passwd</literal>.
+
hashedPasswordFile = mkOption {
+
type = uniq (nullOr types.path);
+
example = "/path/to/file";
+
Specifies the path to a file containing the
+
hashed password for the MQTT user.
+
To generate hashed password install <literal>mosquitto</literal>
+
package and use <literal>mosquitto_passwd</literal>.
+
example = [ "read A/B" "readwrite A/#" ];
+
Control client access to topics on the broker.
+
userAsserts = prefix: users:
+
assertion = builtins.match "[^:\r\n]+" n != null;
+
message = "Invalid user name ${n} in ${prefix}";
+
assertion = count (s: s != null) [
+
u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
+
message = "Cannot set more than one password option for user ${n} in ${prefix}";
+
makePasswordFile = users: path:
+
makeLines = store: file:
+
(n: u: "addLine ${escapeShellArg n} ${escapeShellArg u.${store}}")
+
(filterAttrs (_: u: u.${store} != null) users)
+
(n: u: "addFile ${escapeShellArg n} ${escapeShellArg "${u.${file}}"}")
+
(filterAttrs (_: u: u.${file} != null) users);
+
plainLines = makeLines "password" "passwordFile";
+
hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
+
pkgs.writeScript "make-mosquitto-passwd"
+
#! ${pkgs.runtimeShell}
+
file=${escapeShellArg path}
+
echo "$1:$2" >> "$file"
+
if [ $(wc -l <"$2") -gt 1 ]; then
+
echo "invalid mosquitto password file $2" >&2
+
echo "$1:$(cat "$2")" >> "$file"
+
+ concatStringsSep "\n"
+
++ optional (plainLines != []) ''
+
${pkgs.mosquitto}/bin/mosquitto_passwd -U "$file"
+
makeACLFile = idx: users: supplement:
+
pkgs.writeText "mosquitto-acl-${toString idx}.conf"
+
(n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
+
authPluginOptions = with types; submodule {
+
Plugin path to load, should be a <literal>.so</literal> file.
+
denySpecialChars = mkOption {
+
Automatically disallow all clients using <literal>#</literal>
+
or <literal>+</literal> in their name/id.
+
type = attrsOf optionType;
+
Options for the auth plugin. Each key turns into a <literal>auth_opt_*</literal>
+
authAsserts = prefix: auth:
+
assertion = configKey.check n;
+
message = "Invalid auth plugin key ${prefix}.${n}";
+
formatAuthPlugin = plugin:
+
"auth_plugin ${plugin.plugin}"
+
"auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
+
++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
+
freeformListenerKeys = {
+
allow_zero_length_clientid = 1;
+
require_certificate = 1;
+
tls_engine_kpass_sha1 = 1;
+
use_identity_as_username = 1;
+
use_subject_as_username = 1;
+
use_username_as_clientid = 1;
+
listenerOptions = with types; submodule {
+
Port to listen on. Must be set to 0 to listen on a unix domain socket.
+
Address to listen on. Listen on <literal>0.0.0.0</literal>/<literal>::</literal>
+
authPlugins = mkOption {
+
type = listOf authPluginOptions;
+
Authentication plugin to attach to this listener.
+
Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html">
+
mosquitto.conf documentation</link> for details on authentication plugins.
+
type = attrsOf userOptions;
+
example = { john = { password = "123456"; acl = [ "topic readwrite john/#" ]; }; };
+
A set of users and their passwords and ACLs.
+
Additional ACL items to prepend to the generated ACL file.
+
freeformType = attrsOf optionType;
+
Additional settings for this listener.
+
listenerAsserts = prefix: listener:
+
assertKeysValid prefix freeformListenerKeys listener.settings
+
++ userAsserts prefix listener.users
+
(i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
+
formatListener = idx: listener:
+
"listener ${toString listener.port} ${toString listener.address}"
+
"password_file ${cfg.dataDir}/passwd-${toString idx}"
+
"acl_file ${makeACLFile idx listener.users listener.acl}"
+
++ formatFreeform {} listener.settings
+
++ concatMap formatAuthPlugin listener.authPlugins;
+
bridge_attempt_unsubscribe = 1;
+
bridge_bind_address = 1;
+
bridge_max_packet_size = 1;
+
bridge_outgoing_retain = 1;
+
bridge_protocol_version = 1;
+
bridge_require_ocsp = 1;
+
bridge_tls_version = 1;
+
keepalive_interval = 1;
+
local_cleansession = 1;
+
notification_topic = 1;
+
notifications_local_only = 1;
+
bridgeOptions = with types; submodule {
+
type = listOf (submodule {
+
Address of the remote MQTT broker.
+
Port of the remote MQTT broker.
+
Remote endpoints for the bridge.
+
Topic patterns to be shared between the two brokers.
+
Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html">
+
mosquitto.conf documentation</link> for details on the format.
+
example = [ "# both 2 local/topic/ remote/topic/" ];
+
freeformType = attrsOf optionType;
+
Additional settings for this bridge.
+
bridgeAsserts = prefix: bridge:
+
assertKeysValid prefix freeformBridgeKeys bridge.settings
+
assertion = length bridge.addresses > 0;
+
message = "Bridge ${prefix} needs remote broker addresses";
+
formatBridge = name: bridge:
+
"addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
+
++ map (t: "topic ${t}") bridge.topics
+
++ formatFreeform {} bridge.settings;
+
allow_duplicate_messages = 1;
+
autosave_on_changes = 1;
+
check_retain_source = 1;
+
connection_messages = 1;
+
log_timestamp_format = 1;
+
max_inflight_bytes = 1;
+
max_inflight_messages = 1;
+
max_queued_messages = 1;
+
message_size_limit = 1;
+
persistence_location = 1;
+
persistent_client_expiration = 1;
+
queue_qos0_messages = 1;
+
upgrade_outgoing_qos = 1;
+
websockets_headers_size = 1;
+
websockets_log_level = 1;
+
globalOptions = with types; {
+
enable = mkEnableOption "the MQTT Mosquitto broker";
+
type = attrsOf bridgeOptions;
+
Bridges to build to other MQTT brokers.
+
type = listOf listenerOptions;
+
Listeners to configure on this broker.
+
includeDirs = mkOption {
+
Directories to be scanned for further config files to include.
+
Directories will processed in the order given,
+
<literal>*.conf</literal> files in the directory will be
+
read in case-sensistive alphabetical order.
+
type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
+
Destinations to send log messages to.
+
default = [ "stderr" ];
+
type = listOf (enum [ "debug" "error" "warning" "notice" "information"
+
"subscribe" "unsubscribe" "websockets" "none" "all" ]);
+
Types of messages to log.
+
persistence = mkOption {
+
Enable persistent storage of subscriptions and messages.
+
default = "/var/lib/mosquitto";
+
freeformType = attrsOf optionType;
+
Global configuration options for the mosquitto broker.
+
globalAsserts = prefix: cfg:
+
(assertKeysValid prefix freeformGlobalKeys cfg.settings)
+
(imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
+
(mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
+
"per_listener_settings true"
+
"persistence ${optionToString cfg.persistence}"
+
(d: if path.check d then "log_dest file ${d}" else "log_dest ${d}")
+
++ map (t: "log_type ${t}") cfg.logType
+
++ formatFreeform {} cfg.settings
+
++ concatLists (imap0 formatListener cfg.listeners)
+
++ concatLists (mapAttrsToList formatBridge cfg.bridges)
+
++ map (d: "include_dir ${d}") cfg.includeDirs;
+
configFile = pkgs.writeText "mosquitto.conf"
+
(concatStringsSep "\n" (formatGlobal cfg));
+
options.services.mosquitto = globalOptions;
config = mkIf cfg.enable {
+
assertions = globalAsserts "services.mosquitto" cfg;
systemd.services.mosquitto = {
description = "Mosquitto MQTT Broker Daemon";
···
RuntimeDirectory = "mosquitto";
WorkingDirectory = cfg.dataDir;
+
ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
···
"/tmp" # mosquitto_passwd creates files in /tmp before moving them
+
] ++ filter path.check cfg.logDest;
+
(l.settings.psk_file or null)
+
(l.settings.http_dir or null)
+
(l.settings.cafile or null)
+
(l.settings.capath or null)
+
(l.settings.certfile or null)
+
(l.settings.crlfile or null)
+
(l.settings.dhparamfile or null)
+
(l.settings.keyfile or null)
+
(b.settings.bridge_cafile or null)
+
(b.settings.bridge_capath or null)
+
(b.settings.bridge_certfile or null)
+
(b.settings.bridge_keyfile or null)
RestrictAddressFamilies = [
"AF_UNIX" # for sd_notify() call
···
+
(idx: listener: makePasswordFile listener.users "${cfg.dataDir}/passwd-${toString idx}")
users.users.mosquitto = {
···
users.groups.mosquitto.gid = config.ids.gids.mosquitto;
+
meta.maintainers = with lib.maintainers; [ pennae ];