diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 4bbd4642852..8731683feb9 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -357,6 +357,14 @@
services.multipath.
+
+
+ matrix-conduit,
+ a simple, fast and reliable chat server powered by matrix.
+ Available as
+ services.matrix-conduit.
+
+
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 36d03fd0b59..d101445026e 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -110,6 +110,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [multipath](https://github.com/opensvc/multipath-tools), the device mapper multipath (DM-MP) daemon. Available as [services.multipath](#opt-services.multipath.enable).
+- [matrix-conduit](https://conduit.rs/), a simple, fast and reliable chat server powered by matrix. Available as [services.matrix-conduit](option.html#opt-services.matrix-conduit.enable).
+
## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
- The `services.wakeonlan` option was removed, and replaced with `networking.interfaces..wakeOnLan`.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index df35a57d047..1f855059c81 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -543,6 +543,7 @@
./services/misc/mame.nix
./services/misc/matrix-appservice-discord.nix
./services/misc/matrix-appservice-irc.nix
+ ./services/misc/matrix-conduit.nix
./services/misc/matrix-synapse.nix
./services/misc/mautrix-facebook.nix
./services/misc/mautrix-telegram.nix
diff --git a/nixos/modules/services/misc/matrix-conduit.nix b/nixos/modules/services/misc/matrix-conduit.nix
new file mode 100644
index 00000000000..cabe84ca1ca
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-conduit.nix
@@ -0,0 +1,194 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.matrix-conduit;
+
+ format = pkgs.formats.toml {};
+ configFile = format.generate "conduit.toml" cfg.settings;
+in
+ {
+ meta.maintainers = with maintainers; [ pstn piegames ];
+ options.services.matrix-conduit = {
+ enable = mkEnableOption "matrix-conduit";
+
+ extraEnvironment = mkOption {
+ type = types.attrsOf types.str;
+ description = "Extra Environment variables to pass to the conduit server.";
+ default = {};
+ example = { RUST_BACKTRACE="yes"; };
+ };
+
+ nginx.enable = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Whether to enable a nginx vhost that will listen on all interfaces on tcp/443
+ for https connections and proxy them to conduit. Further nginx configuration
+ can be done by adapting .
+ When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
+ this, set the to
+ false and if appropriate do the same for
+ .
+ '';
+ };
+ package = mkOption {
+ type = types.package;
+ default = pkgs.matrix-conduit;
+ defaultText = "pkgs.matrix-conduit";
+ example = "pkgs.matrix-conduit";
+ description = ''
+ Package of the conduit matrix server to use.
+ '';
+ };
+
+ settings = mkOption {
+ type = types.submodule {
+ freeformType = format.type;
+ options = {
+ global.server_name = mkOption {
+ type = types.str;
+ example = "example.com";
+ description = "The server_name is the name of this server. It is used as a suffix for user # and room ids.";
+ };
+ global.port = mkOption {
+ type = types.port;
+ default = 6167;
+ description = "The port Conduit will be running on. You need to set up a reverse proxy in your web server (e.g. apache or nginx), so all requests to /_matrix on port 443 and 8448 will be forwarded to the Conduit instance running on this port";
+ };
+ global.max_request_size = mkOption {
+ type = types.ints.positive;
+ default = 20000000;
+ description = "Max request size in bytes. Don't forget to also change it in the proxy.";
+ };
+ global.allow_registration = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether new users can register on this server.";
+ };
+ global.allow_encryption = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
+ };
+ global.allow_federation = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether this server federates with other servers.
+ '';
+ };
+ global.trusted_servers = mkOption {
+ type = types.listOf types.str;
+ default = [ "matrix.org" ];
+ description = "Servers trusted with signing server keys.";
+ };
+
+ global.address = mkOption {
+ type = types.str;
+ default = "::1";
+ description = "Address to listen on for connections by the reverse proxy/tls terminator.";
+ };
+ global.database_path = mkOption {
+ type = types.str;
+ default = "/var/lib/matrix-conduit/";
+ readOnly = true;
+ description = ''
+ Path to the conduit database, the directory where conduit will save its data.
+ Note that due to using the DynamicUser feature of systemd, this value should not be changed
+ and is set to be read only.
+ '';
+ };
+ };
+ };
+ default = {};
+ description = ''
+ Generates the conduit.toml configuration file. Refer to
+
+ for details on supported values.
+ Note that database_path can not be edited because the service's reliance on systemd StateDir.
+ '';
+ };
+ };
+
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = !(hasInfix ":" cfg.settings.global.server_name && cfg.nginx.enable);
+ message = ''
+ It appears you are trying to specify a port in the server_name variable.
+ This is not supported by the conduit nginx module. Please define your own vhost.
+ '';
+ }
+ ];
+
+ systemd.services.conduit = {
+ description = "Conduit Matrix Server";
+ documentation = [ "https://gitlab.com/famedly/conduit/" ];
+ wantedBy = [ "multi-user.target" ];
+ environment = lib.mkMerge ([
+ { CONDUIT_CONFIG = configFile; }
+ cfg.extraEnvironment
+ ]);
+ serviceConfig = {
+ DynamicUser = true;
+ User = "conduit";
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateUsers = true;
+ RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged"
+ ];
+ StateDirectory = "matrix-conduit";
+ ExecStart = "${cfg.package}/bin/conduit";
+ Restart = "on-failure";
+ RestartSec = 10;
+ StartLimitBurst = 5;
+ };
+ };
+ services.nginx = mkIf cfg.nginx.enable {
+ enable = mkDefault true;
+ virtualHosts.${cfg.settings.global.server_name} = {
+ enableACME = mkDefault true;
+ forceSSL = mkDefault true;
+ locations."= /.well-known/matrix/server".extraConfig =
+ let
+ server = { "m.server" = "${cfg.settings.global.server_name}:443"; };
+ in if cfg.settings.global.allow_federation then ''
+ add_header Content-Type application/json;
+ return 200 '${builtins.toJSON server}';
+ ''
+ else
+ ''
+ deny all;
+ '';
+
+ locations."/".extraConfig = "deny all;";
+ locations."/_matrix" = let
+ rawUpstream = cfg.settings.global.address;
+ isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
+ upstream = if (isIPv6 rawUpstream) then "[${rawUpstream}]" else rawUpstream;
+ in {
+ proxyPass = "http://${upstream}:${(toString cfg.settings.global.port)}";
+ extraConfig = "client_max_body_size ${(toString cfg.settings.global.max_request_size)};";
+ };
+ };
+ };
+ };
+ }
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 12b67008291..b28147cffee 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -248,6 +248,7 @@ in
mariadb-galera-rsync = handleTest ./mysql/mariadb-galera-rsync.nix {};
matomo = handleTest ./matomo.nix {};
matrix-appservice-irc = handleTest ./matrix-appservice-irc.nix {};
+ matrix-conduit = handleTest ./matrix-conduit.nix {};
matrix-synapse = handleTest ./matrix-synapse.nix {};
mediawiki = handleTest ./mediawiki.nix {};
meilisearch = handleTest ./meilisearch.nix {};
diff --git a/nixos/tests/matrix-conduit.nix b/nixos/tests/matrix-conduit.nix
new file mode 100644
index 00000000000..b028fc63c35
--- /dev/null
+++ b/nixos/tests/matrix-conduit.nix
@@ -0,0 +1,92 @@
+import ./make-test-python.nix ({ pkgs, ... } : let
+
+
+ name = "conduit";
+
+in {
+
+
+ nodes = {
+ conduit = args: {
+ services.matrix-conduit = {
+ enable = true;
+ settings.global.server_name = name;
+ settings.global.allow_registration = true;
+ nginx.enable = true;
+ extraEnvironment.RUST_BACKTRACE = "yes";
+ };
+ services.nginx.virtualHosts."${name}" = {
+ enableACME = false;
+ forceSSL = false;
+ enableSSL = false;
+ };
+ networking.firewall.allowedTCPPorts = [ 80 ];
+ };
+ client = { pkgs, ... }: {
+ environment.systemPackages = [
+ (
+ pkgs.writers.writePython3Bin "do_test"
+ { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
+ import asyncio
+
+ from nio import AsyncClient
+
+
+ async def main() -> None:
+ # Connect to conduit
+ client = AsyncClient("http://conduit:80", "alice")
+
+ # Register as user alice
+ response = await client.register("alice", "my-secret-password")
+
+ # Log in as user alice
+ response = await client.login("my-secret-password")
+
+ # Create a new room
+ response = await client.room_create(federate=False)
+ room_id = response.room_id
+
+ # Join the room
+ response = await client.join(room_id)
+
+ # Send a message to the room
+ response = await client.room_send(
+ room_id=room_id,
+ message_type="m.room.message",
+ content={
+ "msgtype": "m.text",
+ "body": "Hello conduit!"
+ }
+ )
+
+ # Sync responses
+ response = await client.sync(timeout=30000)
+
+ # Check the message was received by conduit
+ last_message = response.rooms.join[room_id].timeline.events[-1].body
+ assert last_message == "Hello conduit!"
+
+ # Leave the room
+ response = await client.room_leave(room_id)
+
+ # Close the client
+ await client.close()
+
+ asyncio.get_event_loop().run_until_complete(main())
+ ''
+ )
+ ];
+ };
+ };
+
+ testScript = ''
+ start_all()
+
+ with subtest("start conduit"):
+ conduit.wait_for_unit("conduit.service")
+ conduit.wait_for_open_port(80)
+
+ with subtest("ensure messages can be exchanged"):
+ client.succeed("do_test")
+ '';
+})