summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2311.section.md20
-rw-r--r--nixos/modules/config/no-x-libs.nix17
-rw-r--r--nixos/modules/i18n/input-method/fcitx5.nix2
-rw-r--r--nixos/modules/module-list.nix3
-rw-r--r--nixos/modules/profiles/installation-device.nix3
-rw-r--r--nixos/modules/programs/firefox.nix2
-rw-r--r--nixos/modules/programs/gamescope.nix4
-rw-r--r--nixos/modules/programs/steam.nix4
-rw-r--r--nixos/modules/security/ipa.nix2
-rw-r--r--nixos/modules/security/pam.nix2
-rw-r--r--nixos/modules/services/audio/wyoming/faster-whisper.nix2
-rw-r--r--nixos/modules/services/cluster/patroni/default.nix2
-rw-r--r--nixos/modules/services/hardware/joycond.nix3
-rw-r--r--nixos/modules/services/hardware/keyd.nix2
-rw-r--r--nixos/modules/services/mail/maddy.nix8
-rw-r--r--nixos/modules/services/matrix/mautrix-whatsapp.nix198
-rw-r--r--nixos/modules/services/misc/gitea.nix1
-rw-r--r--nixos/modules/services/misc/paperless.nix2
-rw-r--r--nixos/modules/services/network-filesystems/eris-server.nix103
-rw-r--r--nixos/modules/services/network-filesystems/kubo.nix6
-rw-r--r--nixos/modules/services/networking/dae.nix41
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix21
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix176
-rw-r--r--nixos/tests/all-tests.nix5
-rw-r--r--nixos/tests/caddy.nix44
-rw-r--r--nixos/tests/dnscrypt-wrapper/default.nix14
-rw-r--r--nixos/tests/eris-server.nix23
-rw-r--r--nixos/tests/kernel-generic.nix6
-rw-r--r--nixos/tests/paperless.nix14
29 files changed, 564 insertions, 166 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md
index 4e8bd3642813b..d87d3b5c92f05 100644
--- a/nixos/doc/manual/release-notes/rl-2311.section.md
+++ b/nixos/doc/manual/release-notes/rl-2311.section.md
@@ -18,6 +18,8 @@
 
 - [wayfire](https://wayfire.org), A modular and extensible wayland compositor. Available as [programs.wayfire](#opt-programs.wayfire.enable).
 
+- [mautrix-whatsapp](https://docs.mau.fi/bridges/go/whatsapp/index.html) A Matrix-WhatsApp puppeting bridge
+
 - [GoToSocial](https://gotosocial.org/), an ActivityPub social network server, written in Golang. Available as [services.gotosocial](#opt-services.gotosocial.enable).
 
 - [Typesense](https://github.com/typesense/typesense), a fast, typo-tolerant search engine for building delightful search experiences. Available as [services.typesense](#opt-services.typesense.enable).
@@ -40,6 +42,8 @@
 
 - [systemd-sysupdate](https://www.freedesktop.org/software/systemd/man/systemd-sysupdate.html), atomically updates the host OS, container images, portable service images or other sources. Available as [systemd.sysupdate](opt-systemd.sysupdate).
 
+- [eris-server](https://codeberg.org/eris/eris-go). [ERIS](https://eris.codeberg.page/) is an encoding for immutable storage and this server provides block exchange as well as content decoding over HTTP and through a FUSE file-system. Available as [services.eris-server](#opt-services.eris-server.enable).
+
 ## Backward Incompatibilities {#sec-release-23.11-incompatibilities}
 
 - The `boot.loader.raspberryPi` options have been marked deprecated, with intent for removal for NixOS 24.11. They had a limited use-case, and do not work like people expect. They required either very old installs ([before mid-2019](https://github.com/NixOS/nixpkgs/pull/62462)) or customized builds out of scope of the standard and generic AArch64 support. That option set never supported the Raspberry Pi 4 family of devices.
@@ -72,6 +76,22 @@
 
 - The [services.caddy.acmeCA](#opt-services.caddy.acmeCA) option now defaults to `null` instead of `"https://acme-v02.api.letsencrypt.org/directory"`, to use all of Caddy's default ACME CAs and enable Caddy's automatic issuer fallback feature by default, as recommended by upstream.
 
+- The default priorities of [`services.nextcloud.phpOptions`](#opt-services.nextcloud.phpOptions) have changed. This means that e.g.
+  `services.nextcloud.phpOptions."opcache.interned_strings_buffer" = "23";` doesn't discard all of the other defaults from this option
+  anymore. The attribute values of `phpOptions` are still defaults, these can be overridden as shown here.
+
+  To override all of the options (including including `upload_max_filesize`, `post_max_size`
+  and `memory_limit` which all point to [`services.nextcloud.maxUploadSize`](#opt-services.nextcloud.maxUploadSize)
+  by default) can be done like this:
+
+  ```nix
+  {
+    services.nextcloud.phpOptions = lib.mkForce {
+      /* ... */
+    };
+  }
+  ```
+
 - `php80` is no longer supported due to upstream not supporting this version anymore.
 
 - PHP now defaults to PHP 8.2, updated from 8.1.
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
index f8622be59a1b0..b2eb46f273b14 100644
--- a/nixos/modules/config/no-x-libs.nix
+++ b/nixos/modules/config/no-x-libs.nix
@@ -26,12 +26,7 @@ with lib;
 
     fonts.fontconfig.enable = false;
 
-    nixpkgs.overlays = singleton (self: super: let
-      packageOverrides = const (python-prev: {
-        # tk feature requires wayland which fails to compile
-        matplotlib = python-prev.matplotlib.override { enableGtk3 = false; enableTk = false; enableQt = false; };
-      });
-    in {
+    nixpkgs.overlays = singleton (const (super: {
       beam = super.beam_nox;
       cairo = super.cairo.override { x11Support = false; };
       dbus = super.dbus.override { x11Support = false; };
@@ -67,8 +62,12 @@ with lib;
       pango = super.pango.override { x11Support = false; };
       pinentry = super.pinentry.override { enabledFlavors = [ "curses" "tty" "emacs" ]; withLibsecret = false; };
       pipewire = super.pipewire.override { x11Support = false; };
-      python3 = super.python3.override { inherit packageOverrides; };
-      python3Packages = self.python3.pkgs; # required otherwise overlays from above are not forwarded
+      pythonPackagesExtensions = super.pythonPackagesExtensions ++ [
+        (python-final: python-prev: {
+          # tk feature requires wayland which fails to compile
+          matplotlib = python-prev.matplotlib.override { enableTk = false; };
+        })
+      ];
       qemu = super.qemu.override { gtkSupport = false; spiceSupport = false; sdlSupport = false; };
       qrencode = super.qrencode.overrideAttrs (_: { doCheck = false; });
       qt5 = super.qt5.overrideScope (const (super': {
@@ -79,6 +78,6 @@ with lib;
       util-linux = super.util-linux.override { translateManpages = false; };
       vim-full = super.vim-full.override { guiSupport = false; };
       zbar = super.zbar.override { enableVideo = false; withXorg = false; };
-    });
+    }));
   };
 }
diff --git a/nixos/modules/i18n/input-method/fcitx5.nix b/nixos/modules/i18n/input-method/fcitx5.nix
index 39952d6c3999e..b72f4be9b57a9 100644
--- a/nixos/modules/i18n/input-method/fcitx5.nix
+++ b/nixos/modules/i18n/input-method/fcitx5.nix
@@ -19,7 +19,7 @@ in
         '';
       };
       quickPhrase = mkOption {
-        type = with types; attrsOf string;
+        type = with types; attrsOf str;
         default = { };
         example = literalExpression ''
           {
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 29fcabaefad51..5852843b8021d 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -600,6 +600,7 @@
   ./services/matrix/dendrite.nix
   ./services/matrix/mautrix-facebook.nix
   ./services/matrix/mautrix-telegram.nix
+  ./services/matrix/mautrix-whatsapp.nix
   ./services/matrix/mjolnir.nix
   ./services/matrix/mx-puppet-discord.nix
   ./services/matrix/pantalaimon.nix
@@ -806,6 +807,7 @@
   ./services/network-filesystems/davfs2.nix
   ./services/network-filesystems/diod.nix
   ./services/network-filesystems/drbd.nix
+  ./services/network-filesystems/eris-server.nix
   ./services/network-filesystems/glusterfs.nix
   ./services/network-filesystems/kbfs.nix
   ./services/network-filesystems/kubo.nix
@@ -863,6 +865,7 @@
   ./services/networking/coturn.nix
   ./services/networking/create_ap.nix
   ./services/networking/croc.nix
+  ./services/networking/dae.nix
   ./services/networking/dante.nix
   ./services/networking/dhcpcd.nix
   ./services/networking/dnscache.nix
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index 4120d5919d7d7..19e7eb32e833f 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -120,5 +120,8 @@ with lib;
       [PStore]
       Unlink=no
     '';
+
+    # allow nix-copy to live system
+    nix.settings.trusted-users = [ "root" "nixos" ];
   };
 }
diff --git a/nixos/modules/programs/firefox.nix b/nixos/modules/programs/firefox.nix
index d67bbee9a7613..8653f066cf8fd 100644
--- a/nixos/modules/programs/firefox.nix
+++ b/nixos/modules/programs/firefox.nix
@@ -53,7 +53,7 @@ in
     };
 
     preferences = mkOption {
-      type = with types; attrsOf (oneOf [ bool int string ]);
+      type = with types; attrsOf (oneOf [ bool int str ]);
       default = { };
       description = mdDoc ''
         Preferences to set from `about:config`.
diff --git a/nixos/modules/programs/gamescope.nix b/nixos/modules/programs/gamescope.nix
index c4424849a41ed..a31295e736df2 100644
--- a/nixos/modules/programs/gamescope.nix
+++ b/nixos/modules/programs/gamescope.nix
@@ -42,7 +42,7 @@ in
     };
 
     args = mkOption {
-      type = types.listOf types.string;
+      type = types.listOf types.str;
       default = [ ];
       example = [ "--rt" "--prefer-vk-device 8086:9bc4" ];
       description = mdDoc ''
@@ -51,7 +51,7 @@ in
     };
 
     env = mkOption {
-      type = types.attrsOf types.string;
+      type = types.attrsOf types.str;
       default = { };
       example = literalExpression ''
         # for Prime render offload on Nvidia laptops.
diff --git a/nixos/modules/programs/steam.nix b/nixos/modules/programs/steam.nix
index c63b31bde11f1..29c449c16946c 100644
--- a/nixos/modules/programs/steam.nix
+++ b/nixos/modules/programs/steam.nix
@@ -89,7 +89,7 @@ in {
         options = {
           enable = mkEnableOption (mdDoc "GameScope Session");
           args = mkOption {
-            type = types.listOf types.string;
+            type = types.listOf types.str;
             default = [ ];
             description = mdDoc ''
               Arguments to be passed to GameScope for the session.
@@ -97,7 +97,7 @@ in {
           };
 
           env = mkOption {
-            type = types.attrsOf types.string;
+            type = types.attrsOf types.str;
             default = { };
             description = mdDoc ''
               Environmental variables to be passed to GameScope for the session.
diff --git a/nixos/modules/security/ipa.nix b/nixos/modules/security/ipa.nix
index 7075be95040ee..69a670cd5e4a3 100644
--- a/nixos/modules/security/ipa.nix
+++ b/nixos/modules/security/ipa.nix
@@ -86,7 +86,7 @@ in {
       };
 
       ifpAllowedUids = mkOption {
-        type = types.listOf types.string;
+        type = types.listOf types.str;
         default = ["root"];
         description = lib.mdDoc "A list of users allowed to access the ifp dbus interface.";
       };
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index ac9da4a823b70..ee260a097c691 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -934,7 +934,7 @@ in
       };
       authserver = mkOption {
         default = null;
-        type = with types; nullOr string;
+        type = with types; nullOr str;
         description = lib.mdDoc ''
           This controls the hostname for the 9front authentication server
           that users will be authenticated against.
diff --git a/nixos/modules/services/audio/wyoming/faster-whisper.nix b/nixos/modules/services/audio/wyoming/faster-whisper.nix
index 6317709b24750..1fb67ecfe5060 100644
--- a/nixos/modules/services/audio/wyoming/faster-whisper.nix
+++ b/nixos/modules/services/audio/wyoming/faster-whisper.nix
@@ -71,7 +71,7 @@ in
               ];
               default = "cpu";
               description = mdDoc ''
-                Id of a speaker in a multi-speaker model.
+                Determines the platform faster-whisper is run on. CPU works everywhere, CUDA requires a compatible NVIDIA GPU.
               '';
             };
 
diff --git a/nixos/modules/services/cluster/patroni/default.nix b/nixos/modules/services/cluster/patroni/default.nix
index 9bf3a285836c0..5ab016a9f59f0 100644
--- a/nixos/modules/services/cluster/patroni/default.nix
+++ b/nixos/modules/services/cluster/patroni/default.nix
@@ -105,7 +105,7 @@ in
     };
 
     otherNodesIps = mkOption {
-      type = types.listOf types.string;
+      type = types.listOf types.str;
       example = [ "192.168.1.2" "192.168.1.3" ];
       description = mdDoc ''
         IP addresses of the other nodes.
diff --git a/nixos/modules/services/hardware/joycond.nix b/nixos/modules/services/hardware/joycond.nix
index 1af18b3b63d37..df3239cb2a7df 100644
--- a/nixos/modules/services/hardware/joycond.nix
+++ b/nixos/modules/services/hardware/joycond.nix
@@ -2,7 +2,6 @@
 
 let
   cfg = config.services.joycond;
-  kernelPackages = config.boot.kernelPackages;
 in
 
 with lib;
@@ -24,8 +23,6 @@ with lib;
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
-    boot.extraModulePackages = optional (versionOlder kernelPackages.kernel.version "5.16") kernelPackages.hid-nintendo;
-
     services.udev.packages = [ cfg.package ];
 
     systemd.packages = [ cfg.package ];
diff --git a/nixos/modules/services/hardware/keyd.nix b/nixos/modules/services/hardware/keyd.nix
index 969383fd4dc78..ead2f456a2024 100644
--- a/nixos/modules/services/hardware/keyd.nix
+++ b/nixos/modules/services/hardware/keyd.nix
@@ -7,7 +7,7 @@ let
   keyboardOptions = { ... }: {
     options = {
       ids = mkOption {
-        type = types.listOf types.string;
+        type = types.listOf types.str;
         default = [ "*" ];
         example = [ "*" "-0123:0456" ];
         description = lib.mdDoc ''
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
index 3b4a517fb8596..2c4d75e8391a4 100644
--- a/nixos/modules/services/mail/maddy.nix
+++ b/nixos/modules/services/mail/maddy.nix
@@ -142,7 +142,7 @@ in {
 
       user = mkOption {
         default = "maddy";
-        type = with types; uniq string;
+        type = with types; uniq str;
         description = lib.mdDoc ''
           User account under which maddy runs.
 
@@ -156,7 +156,7 @@ in {
 
       group = mkOption {
         default = "maddy";
-        type = with types; uniq string;
+        type = with types; uniq str;
         description = lib.mdDoc ''
           Group account under which maddy runs.
 
@@ -170,7 +170,7 @@ in {
 
       hostname = mkOption {
         default = "localhost";
-        type = with types; uniq string;
+        type = with types; uniq str;
         example = ''example.com'';
         description = lib.mdDoc ''
           Hostname to use. It should be FQDN.
@@ -179,7 +179,7 @@ in {
 
       primaryDomain = mkOption {
         default = "localhost";
-        type = with types; uniq string;
+        type = with types; uniq str;
         example = ''mail.example.com'';
         description = lib.mdDoc ''
           Primary MX domain to use. It should be FQDN.
diff --git a/nixos/modules/services/matrix/mautrix-whatsapp.nix b/nixos/modules/services/matrix/mautrix-whatsapp.nix
new file mode 100644
index 0000000000000..80c85980196f3
--- /dev/null
+++ b/nixos/modules/services/matrix/mautrix-whatsapp.nix
@@ -0,0 +1,198 @@
+{
+  lib,
+  config,
+  pkgs,
+  ...
+}: let
+  cfg = config.services.mautrix-whatsapp;
+  dataDir = "/var/lib/mautrix-whatsapp";
+  registrationFile = "${dataDir}/whatsapp-registration.yaml";
+  settingsFile = "${dataDir}/config.json";
+  settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" cfg.settings;
+  settingsFormat = pkgs.formats.json {};
+  appservicePort = 29318;
+in {
+  imports = [];
+  options.services.mautrix-whatsapp = {
+    enable = lib.mkEnableOption "mautrix-whatsapp, a puppeting/relaybot bridge between Matrix and WhatsApp.";
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = {
+        appservice = {
+          address = "http://localhost:${toString appservicePort}";
+          hostname = "[::]";
+          port = appservicePort;
+          database = {
+            type = "sqlite3";
+            uri = "${dataDir}/mautrix-whatsapp.db";
+          };
+          id = "whatsapp";
+          bot = {
+            username = "whatsappbot";
+            displayname = "WhatsApp Bridge Bot";
+          };
+          as_token = "";
+          hs_token = "";
+        };
+        bridge = {
+          username_template = "whatsapp_{{.}}";
+          displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)";
+          double_puppet_server_map = {};
+          login_shared_secret_map = {};
+          command_prefix = "!wa";
+          permissions."*" = "relay";
+          relay.enabled = true;
+        };
+        logging = {
+          min_level = "info";
+          writers = [
+            {
+              type = "stdout";
+              format = "pretty-colored";
+            }
+            {
+              type = "file";
+              format = "json";
+            }
+          ];
+        };
+      };
+      description = lib.mdDoc ''
+        {file}`config.yaml` configuration as a Nix attribute set.
+        Configuration options should match those described in
+        [example-config.yaml](https://github.com/mautrix/whatsapp/blob/master/example-config.yaml).
+        Secret tokens should be specified using {option}`environmentFile`
+        instead of this world-readable attribute set.
+      '';
+      example = {
+        appservice = {
+          database = {
+            type = "postgres";
+            uri = "postgresql:///mautrix_whatsapp?host=/run/postgresql";
+          };
+          id = "whatsapp";
+          ephemeral_events = false;
+        };
+        bridge = {
+          history_sync = {
+            request_full_sync = true;
+          };
+          private_chat_portal_meta = true;
+          mute_bridging = true;
+          encryption = {
+            allow = true;
+            default = true;
+            require = true;
+          };
+          provisioning = {
+            shared_secret = "disable";
+          };
+          permissions = {
+            "example.com" = "user";
+          };
+        };
+      };
+    };
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        File containing environment variables to be passed to the mautrix-whatsapp service,
+        in which secret tokens can be specified securely by optionally defining a value for
+        `MAUTRIX_WHATSAPP_BRIDGE_LOGIN_SHARED_SECRET`.
+      '';
+    };
+
+    serviceDependencies = lib.mkOption {
+      type = with lib.types; listOf str;
+      default = lib.optional config.services.matrix-synapse.enable "matrix-synapse.service";
+      defaultText = lib.literalExpression ''
+        optional config.services.matrix-synapse.enable "matrix-synapse.service"
+      '';
+      description = lib.mdDoc ''
+        List of Systemd services to require and wait for when starting the application service.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.mautrix-whatsapp.settings = {
+      homeserver.domain = lib.mkDefault config.services.matrix-synapse.settings.server_name;
+    };
+
+    systemd.services.mautrix-whatsapp = {
+      description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix";
+
+      wantedBy = ["multi-user.target"];
+      wants = ["network-online.target"] ++ cfg.serviceDependencies;
+      after = ["network-online.target"] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        # substitute the settings file by environment variables
+        # in this case read from EnvironmentFile
+        test -f '${settingsFile}' && rm -f '${settingsFile}'
+        old_umask=$(umask)
+        umask 0177
+        ${pkgs.envsubst}/bin/envsubst \
+          -o '${settingsFile}' \
+          -i '${settingsFileUnsubstituted}'
+        umask $old_umask
+
+        # generate the appservice's registration file if absent
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
+            --generate-registration \
+            --config='${settingsFile}' \
+            --registration='${registrationFile}'
+        fi
+        chmod 640 ${registrationFile}
+
+        umask 0177
+        ${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
+          | .[0].appservice.hs_token = .[1].hs_token
+          | .[0]' '${settingsFile}' '${registrationFile}' \
+          > '${settingsFile}.tmp'
+        mv '${settingsFile}.tmp' '${settingsFile}'
+        umask $old_umask
+      '';
+
+      serviceConfig = {
+        DynamicUser = true;
+        EnvironmentFile = cfg.environmentFile;
+        StateDirectory = baseNameOf dataDir;
+        WorkingDirectory = "${dataDir}";
+        ExecStart = ''
+          ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
+          --config='${settingsFile}' \
+          --registration='${registrationFile}'
+        '';
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        Restart = "on-failure";
+        RestartSec = "30s";
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = ["@system-service"];
+        Type = "simple";
+        UMask = 0027;
+      };
+      restartTriggers = [settingsFileUnsubstituted];
+    };
+  };
+  meta.maintainers = with lib.maintainers; [frederictobiasc];
+}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index ec88de6da3ba8..f6ef2bb919107 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -666,6 +666,7 @@ in
          USER = cfg.user;
          HOME = cfg.stateDir;
          GITEA_WORK_DIR = cfg.stateDir;
+         GITEA_CUSTOM = cfg.customDir;
        };
 
        serviceConfig = {
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 1845d8ad29b57..0683a1f922ab3 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -7,6 +7,7 @@ let
 
   defaultUser = "paperless";
   nltkDir = "/var/cache/paperless/nltk";
+  defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf";
 
   # Don't start a redis instance if the user sets a custom redis connection
   enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
@@ -17,6 +18,7 @@ let
     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
     PAPERLESS_NLTK_DIR = nltkDir;
+    PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont;
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
   } // optionalAttrs (config.time.timeZone != null) {
     PAPERLESS_TIME_ZONE = config.time.timeZone;
diff --git a/nixos/modules/services/network-filesystems/eris-server.nix b/nixos/modules/services/network-filesystems/eris-server.nix
new file mode 100644
index 0000000000000..66eccfac408c4
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/eris-server.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.eris-server;
+  stateDirectoryPath = "\${STATE_DIRECTORY}";
+in {
+
+  options.services.eris-server = {
+
+    enable = lib.mkEnableOption "an ERIS server";
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.eris-go;
+      defaultText = lib.literalExpression "pkgs.eris-go";
+      description = "Package to use for the ERIS server.";
+    };
+
+    decode = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Whether the HTTP service (when enabled) will decode ERIS content at /uri-res/N2R?urn:eris:.
+        Enabling this is recommended only for private or local-only servers.
+      '';
+    };
+
+    listenCoap = lib.mkOption {
+      type = lib.types.str;
+      default = ":5683";
+      example = "[::1]:5683";
+      description = ''
+        Server CoAP listen address. Listen on all IP addresses at port 5683 by default.
+        Please note that the server can service client requests for ERIS-blocks by
+        querying other clients connected to the server. Whether or not blocks are
+        relayed back to the server depends on client configuration but be aware this
+        may leak sensitive metadata and trigger network activity.
+      '';
+    };
+
+    listenHttp = lib.mkOption {
+      type = lib.types.str;
+      default = "";
+      example = "[::1]:8080";
+      description = "Server HTTP listen address. Do not listen by default.";
+    };
+
+    backends = lib.mkOption {
+      type = with lib.types; listOf str;
+      description = ''
+        List of backend URLs.
+        Add "get" and "put" as query elements to enable those operations.
+      '';
+      example = [
+        "bolt+file:///srv/eris.bolt?get&put"
+        "coap+tcp://eris.example.com:5683?get"
+      ];
+    };
+
+    mountpoint = lib.mkOption {
+      type = lib.types.str;
+      default = "";
+      example = "/eris";
+      description = ''
+        Mountpoint for FUSE namespace that exposes "urn:eris:…" files.
+      '';
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.eris-server = let
+      cmd =
+        "${cfg.package}/bin/eris-go server --coap '${cfg.listenCoap}' --http '${cfg.listenHttp}' ${
+          lib.optionalString cfg.decode "--decode "
+        }${
+          lib.optionalString (cfg.mountpoint != "")
+          ''--mountpoint "${cfg.mountpoint}" ''
+        }${lib.strings.escapeShellArgs cfg.backends}";
+    in {
+      description = "ERIS block server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = lib.mkIf (cfg.mountpoint != "") ''
+        export PATH=${config.security.wrapperDir}:$PATH
+        ${cmd}
+      '';
+      serviceConfig = let
+        umounter = lib.mkIf (cfg.mountpoint != "")
+          "-${config.security.wrapperDir}/fusermount -uz ${cfg.mountpoint}";
+      in {
+        ExecStartPre = umounter;
+        ExecStart = lib.mkIf (cfg.mountpoint == "") cmd;
+        ExecStopPost = umounter;
+        Restart = "always";
+        RestartSec = 20;
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ehmry ];
+}
diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix
index a5c370b5be895..5a355f3441d8a 100644
--- a/nixos/modules/services/network-filesystems/kubo.nix
+++ b/nixos/modules/services/network-filesystems/kubo.nix
@@ -278,6 +278,12 @@ in
           You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
         '';
       }
+      {
+        assertion = !((lib.versionAtLeast cfg.package.version "0.21") && (builtins.hasAttr "Experimental" cfg.settings) && (builtins.hasAttr "AcceleratedDHTClient" cfg.settings.Experimental));
+        message = ''
+    The `services.kubo.settings.Experimental.AcceleratedDHTClient` option was renamed to `services.kubo.settings.Routing.AcceleratedDHTClient` in Kubo 0.21.
+  '';
+      }
     ];
 
     environment.systemPackages = [ cfg.package ];
diff --git a/nixos/modules/services/networking/dae.nix b/nixos/modules/services/networking/dae.nix
new file mode 100644
index 0000000000000..b0ad2c0d4bb06
--- /dev/null
+++ b/nixos/modules/services/networking/dae.nix
@@ -0,0 +1,41 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.dae;
+in
+{
+  meta.maintainers = with lib.maintainers; [ pokon548 ];
+
+  options = {
+    services.dae = {
+      enable = lib.options.mkEnableOption (lib.mdDoc "the dae service");
+      package = lib.mkPackageOptionMD pkgs "dae" { };
+    };
+  };
+
+  config = lib.mkIf config.services.dae.enable {
+    networking.firewall.allowedTCPPorts = [ 12345 ];
+    networking.firewall.allowedUDPPorts = [ 12345 ];
+
+    systemd.services.dae = {
+      unitConfig = {
+        Description = "dae Service";
+        Documentation = "https://github.com/daeuniverse/dae";
+        After = [ "network.target" "systemd-sysctl.service" ];
+        Wants = [ "network.target" ];
+      };
+
+      serviceConfig = {
+        User = "root";
+        ExecStartPre = "${lib.getExe cfg.package} validate -c /etc/dae/config.dae";
+        ExecStart = "${lib.getExe cfg.package} run --disable-timestamp -c /etc/dae/config.dae";
+        ExecReload = "${lib.getExe cfg.package} reload $MAINPID";
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        Restart = "on-abnormal";
+        Type = "notify";
+      };
+
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
index 082e0195093ef..741f054cd88be 100644
--- a/nixos/modules/services/networking/dnscrypt-wrapper.nix
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -71,9 +71,9 @@ let
     if ! keyValid; then
       echo "certificate soon to become invalid; backing up old cert"
       mkdir -p oldkeys
-      mv -v ${cfg.providerName}.key oldkeys/${cfg.providerName}-$(date +%F-%T).key
-      mv -v ${cfg.providerName}.crt oldkeys/${cfg.providerName}-$(date +%F-%T).crt
-      systemctl restart dnscrypt-wrapper
+      mv -v "${cfg.providerName}.key" "oldkeys/${cfg.providerName}-$(date +%F-%T).key"
+      mv -v "${cfg.providerName}.crt" "oldkeys/${cfg.providerName}-$(date +%F-%T).crt"
+      kill "$(pidof -s dnscrypt-wrapper)"
     fi
   '';
 
@@ -222,17 +222,6 @@ in {
     };
     users.groups.dnscrypt-wrapper = { };
 
-    security.polkit.extraConfig = ''
-      // Allow dnscrypt-wrapper user to restart dnscrypt-wrapper.service
-      polkit.addRule(function(action, subject) {
-          if (action.id == "org.freedesktop.systemd1.manage-units" &&
-              action.lookup("unit") == "dnscrypt-wrapper.service" &&
-              subject.user == "dnscrypt-wrapper") {
-              return polkit.Result.YES;
-          }
-        });
-    '';
-
     systemd.services.dnscrypt-wrapper = {
       description = "dnscrypt-wrapper daemon";
       after    = [ "network.target" ];
@@ -242,7 +231,7 @@ in {
       serviceConfig = {
         User = "dnscrypt-wrapper";
         WorkingDirectory = dataDir;
-        Restart   = "on-failure";
+        Restart   = "always";
         ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}";
       };
 
@@ -255,7 +244,7 @@ in {
       requires = [ "dnscrypt-wrapper.service" ];
       description = "Rotates DNSCrypt wrapper keys if soon to expire";
 
-      path   = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy1 gawk ];
+      path   = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy1 gawk procps ];
       script = rotateKeys;
       serviceConfig.User = "dnscrypt-wrapper";
     };
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 06af9d933e084..e0a7e7d4859c8 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -8,6 +8,21 @@ let
 
   jsonFormat = pkgs.formats.json {};
 
+  defaultPHPSettings = {
+    short_open_tag = "Off";
+    expose_php = "Off";
+    error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
+    display_errors = "stderr";
+    "opcache.enable_cli" = "1";
+    "opcache.interned_strings_buffer" = "8";
+    "opcache.max_accelerated_files" = "10000";
+    "opcache.memory_consumption" = "128";
+    "opcache.revalidate_freq" = "1";
+    "opcache.fast_shutdown" = "1";
+    "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
+    catch_workers_output = "yes";
+  };
+
   inherit (cfg) datadir;
 
   phpPackage = cfg.phpPackage.buildEnv {
@@ -26,22 +41,13 @@ let
         ++ optional cfg.caching.memcached memcached
       )
       ++ cfg.phpExtraExtensions all; # Enabled by user
-    extraConfig = toKeyValue phpOptions;
+    extraConfig = toKeyValue cfg.phpOptions;
   };
 
   toKeyValue = generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {} " = ";
   };
 
-  phpOptions = {
-    upload_max_filesize = cfg.maxUploadSize;
-    post_max_size = cfg.maxUploadSize;
-    memory_limit = cfg.maxUploadSize;
-  } // cfg.phpOptions
-    // optionalAttrs cfg.caching.apcu {
-      "apc.enable_cli" = "1";
-    };
-
   occ = pkgs.writeScriptBin "nextcloud-occ" ''
     #! ${pkgs.runtimeShell}
     cd ${cfg.package}
@@ -136,8 +142,8 @@ in {
       default = config.services.nextcloud.home;
       defaultText = literalExpression "config.services.nextcloud.home";
       description = lib.mdDoc ''
-        Data storage path of nextcloud.  Will be [](#opt-services.nextcloud.home) by default.
-        This folder will be populated with a config.php and data folder which contains the state of the instance (excl the database).";
+        Nextcloud's data storage path.  Will be [](#opt-services.nextcloud.home) by default.
+        This folder will be populated with a config.php file and a data folder which contains the state of the instance (excluding the database).";
       '';
       example = "/mnt/nextcloud-file";
     };
@@ -170,8 +176,8 @@ in {
       type = types.bool;
       default = true;
       description = lib.mdDoc ''
-        Automatically enable the apps in [](#opt-services.nextcloud.extraApps) every time nextcloud starts.
-        If set to false, apps need to be enabled in the Nextcloud user interface or with nextcloud-occ app:enable.
+        Automatically enable the apps in [](#opt-services.nextcloud.extraApps) every time Nextcloud starts.
+        If set to false, apps need to be enabled in the Nextcloud web user interface or with `nextcloud-occ app:enable`.
       '';
     };
     appstoreEnable = mkOption {
@@ -179,16 +185,28 @@ in {
       default = null;
       example = true;
       description = lib.mdDoc ''
-        Allow the installation of apps and app updates from the store.
+        Allow the installation and updating of apps from the Nextcloud appstore.
         Enabled by default unless there are packages in [](#opt-services.nextcloud.extraApps).
-        Set to true to force enable the store even if [](#opt-services.nextcloud.extraApps) is used.
-        Set to false to disable the installation of apps from the global appstore. App management is always enabled regardless of this setting.
+        Set this to true to force enable the store even if [](#opt-services.nextcloud.extraApps) is used.
+        Set this to false to disable the installation of apps from the global appstore. App management is always enabled regardless of this setting.
       '';
     };
     logLevel = mkOption {
       type = types.ints.between 0 4;
       default = 2;
-      description = lib.mdDoc "Log level value between 0 (DEBUG) and 4 (FATAL).";
+      description = lib.mdDoc ''
+        Log level value between 0 (DEBUG) and 4 (FATAL).
+
+        - 0 (debug): Log all activity.
+
+        - 1 (info): Log activity such as user logins and file activities, plus warnings, errors, and fatal errors.
+
+        - 2 (warn): Log successful operations, as well as warnings of potential problems, errors and fatal errors.
+
+        - 3 (error): Log failed operations and fatal errors.
+
+        - 4 (fatal): Log only fatal errors that cause the server to stop.
+      '';
     };
     logType = mkOption {
       type = types.enum [ "errorlog" "file" "syslog" "systemd" ];
@@ -202,7 +220,7 @@ in {
     https = mkOption {
       type = types.bool;
       default = false;
-      description = lib.mdDoc "Use https for generated links.";
+      description = lib.mdDoc "Use HTTPS for generated links.";
     };
     package = mkOption {
       type = types.package;
@@ -222,7 +240,7 @@ in {
       default = "512M";
       type = types.str;
       description = lib.mdDoc ''
-        Defines the upload limit for files. This changes the relevant options
+        The upload limit for files. This changes the relevant options
         in php.ini and nginx if enabled.
       '';
     };
@@ -251,10 +269,10 @@ in {
       default = all: [];
       defaultText = literalExpression "all: []";
       description = lib.mdDoc ''
-        Additional PHP extensions to use for nextcloud.
-        By default, only extensions necessary for a vanilla nextcloud installation are enabled,
+        Additional PHP extensions to use for Nextcloud.
+        By default, only extensions necessary for a vanilla Nextcloud installation are enabled,
         but you may choose from the list of available extensions and add further ones.
-        This is sometimes necessary to be able to install a certain nextcloud app that has additional requirements.
+        This is sometimes necessary to be able to install a certain Nextcloud app that has additional requirements.
       '';
       example = literalExpression ''
         all: [ all.pdlib all.bz2 ]
@@ -263,22 +281,33 @@ in {
 
     phpOptions = mkOption {
       type = types.attrsOf types.str;
-      default = {
-        short_open_tag = "Off";
-        expose_php = "Off";
-        error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
-        display_errors = "stderr";
-        "opcache.enable_cli" = "1";
-        "opcache.interned_strings_buffer" = "8";
-        "opcache.max_accelerated_files" = "10000";
-        "opcache.memory_consumption" = "128";
-        "opcache.revalidate_freq" = "1";
-        "opcache.fast_shutdown" = "1";
-        "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
-        catch_workers_output = "yes";
-      };
+      defaultText = literalExpression (generators.toPretty { } defaultPHPSettings);
       description = lib.mdDoc ''
         Options for PHP's php.ini file for nextcloud.
+
+        Please note that this option is _additive_ on purpose while the
+        attribute values inside the default are option defaults: that means that
+
+        ```nix
+        {
+          services.nextcloud.phpOptions."opcache.interned_strings_buffer" = "23";
+        }
+        ```
+
+        will override the `php.ini` option `opcache.interned_strings_buffer` without
+        discarding the rest of the defaults.
+
+        Overriding all of `phpOptions` (including `upload_max_filesize`, `post_max_size`
+        and `memory_limit` which all point to [](#opt-services.nextcloud.maxUploadSize)
+        by default) can be done like this:
+
+        ```nix
+        {
+          services.nextcloud.phpOptions = lib.mkForce {
+            /* ... */
+          };
+        }
+        ```
       '';
     };
 
@@ -301,7 +330,7 @@ in {
       type = types.nullOr types.lines;
       default = null;
       description = lib.mdDoc ''
-        Options for nextcloud's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
+        Options for Nextcloud's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
       '';
     };
 
@@ -319,7 +348,7 @@ in {
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Create the database and database user locally.
+          Whether to create the database and database user locally.
         '';
       };
 
@@ -357,9 +386,10 @@ in {
           else "localhost";
         defaultText = "localhost";
         description = lib.mdDoc ''
-          Database host or socket path. Defaults to the correct unix socket
-          instead if `services.nextcloud.database.createLocally` is true and
-          `services.nextcloud.config.dbtype` is either `pgsql` or `mysql`.
+          Database host or socket path.
+          If [](#opt-services.nextcloud.database.createLocally) is true and
+          [](#opt-services.nextcloud.config.dbtype) is either `pgsql` or `mysql`,
+          defaults to the correct Unix socket instead.
         '';
       };
       dbport = mkOption {
@@ -370,19 +400,23 @@ in {
       dbtableprefix = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = lib.mdDoc "Table prefix in Nextcloud database.";
+        description = lib.mdDoc "Table prefix in Nextcloud's database.";
       };
       adminuser = mkOption {
         type = types.str;
         default = "root";
-        description = lib.mdDoc "Admin username.";
+        description = lib.mdDoc ''
+          Username for the admin account. The username is only set during the
+          initial setup of Nextcloud! Since the username also acts as unique
+          ID internally, it cannot be changed later!
+        '';
       };
       adminpassFile = mkOption {
         type = types.str;
         description = lib.mdDoc ''
           The full path to a file that contains the admin's password. Must be
           readable by user `nextcloud`. The password is set only in the initial
-          setup of nextcloud by the systemd `nextcloud-setup.service`.
+          setup of Nextcloud by the systemd service `nextcloud-setup.service`.
         '';
       };
 
@@ -390,7 +424,7 @@ in {
         type = types.listOf types.str;
         default = [];
         description = lib.mdDoc ''
-          Trusted domains, from which the nextcloud installation will be
+          Trusted domains from which the Nextcloud installation will be
           accessible.  You don't need to add
           `services.nextcloud.hostname` here.
         '';
@@ -400,8 +434,8 @@ in {
         type = types.listOf types.str;
         default = [];
         description = lib.mdDoc ''
-          Trusted proxies, to provide if the nextcloud installation is being
-          proxied to secure against e.g. spoofing.
+          Trusted proxies to provide if the Nextcloud installation is being
+          proxied to secure against, e.g. spoofing.
         '';
       };
 
@@ -411,10 +445,10 @@ in {
         example = "https";
 
         description = lib.mdDoc ''
-          Force Nextcloud to always use HTTPS i.e. for link generation. Nextcloud
-          uses the currently used protocol by default, but when behind a reverse-proxy,
-          it may use `http` for everything although Nextcloud
-          may be served via HTTPS.
+          Force Nextcloud to always use HTTP or HTTPS i.e. for link generation.
+          Nextcloud uses the currently used protocol by default, but when
+          behind a reverse-proxy, it may use `http` for everything although
+          Nextcloud may be served via HTTPS.
         '';
       };
 
@@ -423,16 +457,12 @@ in {
         type = types.nullOr types.str;
         example = "DE";
         description = lib.mdDoc ''
-          ::: {.warning}
-          This option exists since Nextcloud 21! If older versions are used,
-          this will throw an eval-error!
-          :::
-
-          [ISO 3611-1](https://www.iso.org/iso-3166-country-codes.html)
-          country codes for automatic phone-number detection without a country code.
+          An [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html)
+          country code which replaces automatic phone-number detection
+          without a country code.
 
-          With e.g. `DE` set, the `+49` can be omitted for
-          phone-numbers.
+          As an example, with `DE` set as the default phone region,
+          the `+49` prefix can be omitted for phone numbers.
         '';
       };
 
@@ -557,10 +587,10 @@ in {
       default = config.services.nextcloud.notify_push.enable;
       defaultText = literalExpression "config.services.nextcloud.notify_push.enable";
       description = lib.mdDoc ''
-        Whether to configure nextcloud to use the recommended redis settings for small instances.
+        Whether to configure Nextcloud to use the recommended Redis settings for small instances.
 
         ::: {.note}
-        The `notify_push` app requires redis to be configured. If this option is turned off, this must be configured manually.
+        The `notify_push` app requires Redis to be configured. If this option is turned off, this must be configured manually.
         :::
       '';
     };
@@ -597,7 +627,7 @@ in {
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Run regular auto update of all apps installed from the nextcloud app store.
+          Run a regular auto-update of all apps installed from the Nextcloud app store.
         '';
       };
       startAt = mkOption {
@@ -644,7 +674,7 @@ in {
       type = jsonFormat.type;
       default = {};
       description = lib.mdDoc ''
-        Extra options which should be appended to nextcloud's config.php file.
+        Extra options which should be appended to Nextcloud's config.php file.
       '';
       example = literalExpression '' {
         redis = {
@@ -661,7 +691,7 @@ in {
       type = types.nullOr types.str;
       default = null;
       description = lib.mdDoc ''
-        Secret options which will be appended to nextcloud's config.php file (written as JSON, in the same
+        Secret options which will be appended to Nextcloud's config.php file (written as JSON, in the same
         form as the [](#opt-services.nextcloud.extraOptions) option), for example
         `{"redis":{"password":"secret"}}`.
       '';
@@ -695,7 +725,7 @@ in {
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
 
             After nextcloud${toString major} is installed successfully, you can safely upgrade
-            to ${toString (major + 1)}. The latest version available is nextcloud${toString latest}.
+            to ${toString (major + 1)}. The latest version available is Nextcloud${toString latest}.
 
             Please note that Nextcloud doesn't support upgrades across multiple major versions
             (i.e. an upgrade from 16 is possible to 17, but not 16 to 18).
@@ -750,6 +780,18 @@ in {
       services.nextcloud.phpPackage =
         if versionOlder cfg.package.version "26" then pkgs.php81
         else pkgs.php82;
+
+      services.nextcloud.phpOptions = mkMerge [
+        (mapAttrs (const mkOptionDefault) defaultPHPSettings)
+        {
+          upload_max_filesize = cfg.maxUploadSize;
+          post_max_size = cfg.maxUploadSize;
+          memory_limit = cfg.maxUploadSize;
+        }
+        (mkIf cfg.caching.apcu {
+          "apc.enable_cli" = "1";
+        })
+      ];
     }
 
     { assertions = [
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 3b4a39f5ff96b..a2235b106dc64 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -21,7 +21,7 @@ let
     if isAttrs val
     then
       if hasAttr "test" val then callTest val
-      else mapAttrs (n: s: discoverTests s) val
+      else mapAttrs (n: s: if n == "passthru" then s else discoverTests s) val
     else if isFunction val
       then
         # Tests based on make-test-python.nix will return the second lambda
@@ -217,7 +217,7 @@ in {
   disable-installer-tools = handleTest ./disable-installer-tools.nix {};
   discourse = handleTest ./discourse.nix {};
   dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {};
-  dnscrypt-wrapper = handleTestOn ["x86_64-linux"] ./dnscrypt-wrapper {};
+  dnscrypt-wrapper = runTestOn ["x86_64-linux"] ./dnscrypt-wrapper;
   dnsdist = handleTest ./dnsdist.nix {};
   doas = handleTest ./doas.nix {};
   docker = handleTestOn ["aarch64-linux" "x86_64-linux"] ./docker.nix {};
@@ -252,6 +252,7 @@ in {
   envoy = handleTest ./envoy.nix {};
   ergo = handleTest ./ergo.nix {};
   ergochat = handleTest ./ergochat.nix {};
+  eris-server = handleTest ./eris-server.nix {};
   esphome = handleTest ./esphome.nix {};
   etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
   activation = pkgs.callPackage ../modules/system/activation/test.nix { };
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
index ed88f08739e85..238091ec606f5 100644
--- a/nixos/tests/caddy.nix
+++ b/nixos/tests/caddy.nix
@@ -22,22 +22,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       '';
       services.caddy.enableReload = true;
 
-      specialisation.etag.configuration = {
-        services.caddy.extraConfig = lib.mkForce ''
-          http://localhost {
-            encode gzip
-
-            file_server
-            root * ${
-              pkgs.runCommand "testdir2" {} ''
-                mkdir "$out"
-                echo changed > "$out/example.html"
-              ''
-            }
-          }
-        '';
-      };
-
       specialisation.config-reload.configuration = {
         services.caddy.extraConfig = ''
           http://localhost:8080 {
@@ -55,7 +39,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   testScript = { nodes, ... }:
     let
-      etagSystem = "${nodes.webserver.system.build.toplevel}/specialisation/etag";
       justReloadSystem = "${nodes.webserver.system.build.toplevel}/specialisation/config-reload";
       multipleConfigs = "${nodes.webserver.system.build.toplevel}/specialisation/multiple-configs";
     in
@@ -65,33 +48,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       webserver.wait_for_open_port(80)
 
 
-      def check_etag(url):
-          etag = webserver.succeed(
-              "curl --fail -v '{}' 2>&1 | sed -n -e \"s/^< [Ee][Tt][Aa][Gg]: *//p\"".format(
-                  url
-              )
-          )
-          etag = etag.replace("\r\n", " ")
-          http_code = webserver.succeed(
-              "curl --fail --silent --show-error -o /dev/null -w \"%{{http_code}}\" --head -H 'If-None-Match: {}' {}".format(
-                  etag, url
-              )
-          )
-          assert int(http_code) == 304, "HTTP code is {}, expected 304".format(http_code)
-          return etag
-
-
-      with subtest("check ETag if serving Nix store paths"):
-          old_etag = check_etag(url)
-          webserver.succeed(
-              "${etagSystem}/bin/switch-to-configuration test >&2"
-          )
-          webserver.sleep(1)
-          new_etag = check_etag(url)
-          assert old_etag != new_etag, "Old ETag {} is the same as {}".format(
-              old_etag, new_etag
-          )
-
       with subtest("config is reloaded on nixos-rebuild switch"):
           webserver.succeed(
               "${justReloadSystem}/bin/switch-to-configuration test >&2"
diff --git a/nixos/tests/dnscrypt-wrapper/default.nix b/nixos/tests/dnscrypt-wrapper/default.nix
index 1bdd064e1130c..1c05376e097b3 100644
--- a/nixos/tests/dnscrypt-wrapper/default.nix
+++ b/nixos/tests/dnscrypt-wrapper/default.nix
@@ -1,4 +1,6 @@
-import ../make-test-python.nix ({ pkgs, ... }: {
+{ lib, pkgs, ... }:
+
+{
   name = "dnscrypt-wrapper";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ rnhmjoj ];
@@ -50,23 +52,23 @@ import ../make-test-python.nix ({ pkgs, ... }: {
         server.wait_for_unit("dnscrypt-wrapper")
         server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.key")
         server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.crt")
+        almost_expiration = server.succeed("date --date '4days 23 hours 56min'").strip()
 
     with subtest("The client can connect to the server"):
         server.wait_for_unit("tinydns")
         client.wait_for_unit("dnscrypt-proxy2")
-        assert "1.2.3.4" in client.succeed(
+        assert "1.2.3.4" in client.wait_until_succeeds(
             "host it.works"
         ), "The IP address of 'it.works' does not match 1.2.3.4"
 
     with subtest("The server rotates the ephemeral keys"):
         # advance time by a little less than 5 days
-        server.succeed("date -s \"$(date --date '4 days 6 hours')\"")
-        client.succeed("date -s \"$(date --date '4 days 6 hours')\"")
+        server.succeed(f"date -s '{almost_expiration}'")
+        client.succeed(f"date -s '{almost_expiration}'")
         server.wait_for_file("/var/lib/dnscrypt-wrapper/oldkeys")
 
     with subtest("The client can still connect to the server"):
         server.wait_for_unit("dnscrypt-wrapper")
         client.succeed("host it.works")
   '';
-})
-
+}
diff --git a/nixos/tests/eris-server.nix b/nixos/tests/eris-server.nix
new file mode 100644
index 0000000000000..a50db3afebf5f
--- /dev/null
+++ b/nixos/tests/eris-server.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "eris-server";
+  meta.maintainers = with lib.maintainers; [ ehmry ];
+
+  nodes.server = {
+    environment.systemPackages = [ pkgs.eris-go pkgs.nim.pkgs.eris ];
+    services.eris-server = {
+      enable = true;
+      decode = true;
+      listenHttp = "[::1]:80";
+      backends = [ "badger+file:///var/cache/eris.badger?get&put" ];
+      mountpoint = "/eris";
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("eris-server.service")
+    server.wait_for_open_port(5683)
+    server.wait_for_open_port(80)
+    server.succeed("eriscmd get http://[::1] $(echo 'Hail ERIS!' | eriscmd put coap+tcp://[::1]:5683)")
+  '';
+})
diff --git a/nixos/tests/kernel-generic.nix b/nixos/tests/kernel-generic.nix
index e4a8e06df1ef3..e69dd550289c1 100644
--- a/nixos/tests/kernel-generic.nix
+++ b/nixos/tests/kernel-generic.nix
@@ -42,7 +42,9 @@ let
   };
 
 in mapAttrs (_: lP: testsForLinuxPackages lP) kernels // {
-  inherit testsForLinuxPackages;
+  passthru = {
+    inherit testsForLinuxPackages;
 
-  testsForKernel = kernel: testsForLinuxPackages (pkgs.linuxPackagesFor kernel);
+    testsForKernel = kernel: testsForLinuxPackages (pkgs.linuxPackagesFor kernel);
+  };
 }
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
index 7f36de4c29b71..ce6a4d8128dfd 100644
--- a/nixos/tests/paperless.nix
+++ b/nixos/tests/paperless.nix
@@ -30,20 +30,27 @@ import ./make-test-python.nix ({ lib, ... }: {
     with subtest("Task-queue gets ready"):
         machine.wait_for_unit("paperless-task-queue.service")
 
-    with subtest("Add a document via the web interface"):
+    with subtest("Add a png document via the web interface"):
         machine.succeed(
             "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
             "-annotate +5+20 'hello web 16-10-2005' /tmp/webdoc.png"
         )
         machine.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.png -fs localhost:28981/api/documents/post_document/")
 
+    with subtest("Add a txt document via the web interface"):
+        machine.succeed(
+            "echo 'hello web 16-10-2005' > /tmp/webdoc.txt"
+        )
+        machine.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.txt -fs localhost:28981/api/documents/post_document/")
+
     with subtest("Documents are consumed"):
         machine.wait_until_succeeds(
-            "(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 2))"
+            "(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 3))"
         )
         docs = json.loads(machine.succeed("curl -u admin:admin -fs localhost:28981/api/documents/"))['results']
         assert "2005-10-16" in docs[0]['created']
         assert "2005-10-16" in docs[1]['created']
+        assert "2005-10-16" in docs[2]['created']
 
     # Detects gunicorn issues, see PR #190888
     with subtest("Document metadata can be accessed"):
@@ -52,5 +59,8 @@ import ./make-test-python.nix ({ lib, ... }: {
 
         metadata = json.loads(machine.succeed("curl -u admin:admin -fs localhost:28981/api/documents/2/metadata/"))
         assert "original_checksum" in metadata
+
+        metadata = json.loads(machine.succeed("curl -u admin:admin -fs localhost:28981/api/documents/3/metadata/"))
+        assert "original_checksum" in metadata
   '';
 })