summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md6
-rw-r--r--nixos/doc/manual/development/nixos-tests.xml1
-rw-r--r--nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml10
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml69
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md15
-rw-r--r--nixos/lib/testing-python.nix9
-rw-r--r--nixos/modules/hardware/all-firmware.nix13
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix6
-rw-r--r--nixos/modules/installer/netboot/netboot.nix6
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix10
-rw-r--r--nixos/modules/misc/ids.nix1
-rw-r--r--nixos/modules/misc/version.nix5
-rw-r--r--nixos/modules/module-list.nix5
-rw-r--r--nixos/modules/programs/captive-browser.nix2
-rw-r--r--nixos/modules/services/admin/meshcentral.nix53
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix54
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix8
-rw-r--r--nixos/modules/services/databases/redis.nix2
-rw-r--r--nixos/modules/services/hardware/sane.nix27
-rw-r--r--nixos/modules/services/misc/gitea.nix11
-rw-r--r--nixos/modules/services/misc/libreddit.nix66
-rw-r--r--nixos/modules/services/misc/mx-puppet-discord.nix120
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix1
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix34
-rw-r--r--nixos/modules/services/network-filesystems/litestream/default.nix100
-rw-r--r--nixos/modules/services/network-filesystems/litestream/litestream.xml65
-rw-r--r--nixos/modules/services/networking/bird.nix4
-rw-r--r--nixos/modules/services/networking/firewall.nix4
-rw-r--r--nixos/modules/services/networking/kea.nix16
-rw-r--r--nixos/modules/services/networking/networkmanager.nix13
-rw-r--r--nixos/modules/services/networking/nftables.nix1
-rw-r--r--nixos/modules/services/networking/syncthing.nix634
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix3
-rw-r--r--nixos/modules/services/system/uptimed.nix10
-rw-r--r--nixos/modules/services/web-apps/node-red.nix148
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix127
-rwxr-xr-xnixos/modules/services/x11/display-managers/set-session.py3
-rw-r--r--nixos/modules/system/boot/systemd.nix3
-rw-r--r--nixos/modules/system/etc/etc.nix64
-rw-r--r--nixos/modules/system/etc/make-etc.sh45
-rw-r--r--nixos/modules/virtualisation/virtualbox-image.nix8
-rw-r--r--nixos/tests/all-tests.nix5
-rw-r--r--nixos/tests/gitlab.nix12
-rw-r--r--nixos/tests/kbd-update-search-paths-patch.nix18
-rw-r--r--nixos/tests/libreddit.nix19
-rw-r--r--nixos/tests/litestream.nix93
-rw-r--r--nixos/tests/mpv.nix28
-rw-r--r--nixos/tests/node-red.nix31
-rw-r--r--nixos/tests/prometheus-exporters.nix20
-rw-r--r--nixos/tests/rabbitmq.nix2
-rw-r--r--nixos/tests/soapui.nix24
-rw-r--r--nixos/tests/syncthing-init.nix16
-rw-r--r--nixos/tests/syncthing.nix4
55 files changed, 1576 insertions, 482 deletions
diff --git a/nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md b/nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md
new file mode 100644
index 00000000000..38a64027f7c
--- /dev/null
+++ b/nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md
@@ -0,0 +1,6 @@
+# Linking NixOS tests to packages {#sec-linking-nixos-tests-to-packages}
+
+You can link NixOS module tests to the packages that they exercised,
+so that the tests can be run automatically during code review when the package gets changed.
+This is
+[described in the nixpkgs manual](https://nixos.org/manual/nixpkgs/stable/#ssec-nixos-tests-linking).
diff --git a/nixos/doc/manual/development/nixos-tests.xml b/nixos/doc/manual/development/nixos-tests.xml
index 702fc03f668..67dc09fc715 100644
--- a/nixos/doc/manual/development/nixos-tests.xml
+++ b/nixos/doc/manual/development/nixos-tests.xml
@@ -16,4 +16,5 @@ xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/tests">nixos/test
  <xi:include href="../from_md/development/writing-nixos-tests.section.xml" />
  <xi:include href="../from_md/development/running-nixos-tests.section.xml" />
  <xi:include href="../from_md/development/running-nixos-tests-interactively.section.xml" />
+ <xi:include href="../from_md/development/linking-nixos-tests-to-packages.section.xml" />
 </chapter>
diff --git a/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml b/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml
new file mode 100644
index 00000000000..666bbec6162
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml
@@ -0,0 +1,10 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-linking-nixos-tests-to-packages">
+  <title>Linking NixOS tests to packages</title>
+  <para>
+    You can link NixOS module tests to the packages that they exercised,
+    so that the tests can be run automatically during code review when
+    the package gets changed. This is
+    <link xlink:href="https://nixos.org/manual/nixpkgs/stable/#ssec-nixos-tests-linking">described
+    in the nixpkgs manual</link>.
+  </para>
+</section>
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 9dd98a5262f..2874039e3f0 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
@@ -125,6 +125,30 @@
           <link linkend="opt-services.prometheus.exporters.buildkite-agent.enable">services.prometheus.exporters.buildkite-agent</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/prometheus/influxdb_exporter">influxdb-exporter</link>
+          a Prometheus exporter that exports metrics received on an
+          InfluxDB compatible endpoint is now available as
+          <link linkend="opt-services.prometheus.exporters.influxdb.enable">services.prometheus.exporters.influxdb</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/matrix-discord/mx-puppet-discord">mx-puppet-discord</link>,
+          a discord puppeting bridge for matrix. Available as
+          <link linkend="opt-services.mx-puppet-discord.enable">services.mx-puppet-discord</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.meshcommander.com/meshcentral2/overview">MeshCentral</link>,
+          a remote administration service (<quote>TeamViewer but
+          self-hosted and with more features</quote>) is now available
+          with a package and a module:
+          <link linkend="opt-services.meshcentral.enable">services.meshcentral.enable</link>
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-21.11-incompatibilities">
@@ -555,6 +579,51 @@
           6.0.0 to 9.0.0
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <literal>tt-rss</literal> was upgraded to the commit on
+          2021-06-21, which has breaking changes. If you use
+          <literal>services.tt-rss.extraConfig</literal> you should
+          migrate to the <literal>putenv</literal>-style configuration.
+          See
+          <link xlink:href="https://community.tt-rss.org/t/rip-config-php-hello-classes-config-php/4337">this
+          Discourse post</link> in the tt-rss forums for more details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The following Visual Studio Code extensions were renamed to
+          keep the naming convention uniform.
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              <literal>bbenoist.Nix</literal> -&gt;
+              <literal>bbenoist.nix</literal>
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>CoenraadS.bracket-pair-colorizer</literal> -&gt;
+              <literal>coenraads.bracket-pair-colorizer</literal>
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>golang.Go</literal> -&gt;
+              <literal>golang.go</literal>
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.uptimed</literal> now uses
+          <literal>/var/lib/uptimed</literal> as its stateDirectory
+          instead of <literal>/var/spool/uptimed</literal>. Make sure to
+          move all files to the new directory.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-21.11-notable-changes">
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 12e1a938433..fe7a7326a40 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -39,6 +39,12 @@ pt-services.clipcat.enable).
 
 - [buildkite-agent-metrics](https://github.com/buildkite/buildkite-agent-metrics), a command-line tool for collecting Buildkite agent metrics, now has a Prometheus exporter available as [services.prometheus.exporters.buildkite-agent](#opt-services.prometheus.exporters.buildkite-agent.enable).
 
+- [influxdb-exporter](https://github.com/prometheus/influxdb_exporter) a Prometheus exporter that exports metrics received on an InfluxDB compatible endpoint is now available as [services.prometheus.exporters.influxdb](#opt-services.prometheus.exporters.influxdb.enable).
+
+- [mx-puppet-discord](https://github.com/matrix-discord/mx-puppet-discord), a discord puppeting bridge for matrix. Available as [services.mx-puppet-discord](#opt-services.mx-puppet-discord.enable).
+
+- [MeshCentral](https://www.meshcommander.com/meshcentral2/overview), a remote administration service ("TeamViewer but self-hosted and with more features") is now available with a package and a module: [services.meshcentral.enable](#opt-services.meshcentral.enable)
+
 ## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
 
 - The `staticjinja` package has been upgraded from 1.0.4 to 3.0.1
@@ -142,6 +148,15 @@ pt-services.clipcat.enable).
 
 - the `mingw-64` package has been upgraded from 6.0.0 to 9.0.0
 
+- `tt-rss` was upgraded to the commit on 2021-06-21, which has breaking changes. If you use `services.tt-rss.extraConfig` you should migrate to the `putenv`-style configuration. See [this Discourse post](https://community.tt-rss.org/t/rip-config-php-hello-classes-config-php/4337) in the tt-rss forums for more details.
+
+- The following Visual Studio Code extensions were renamed to keep the naming convention uniform.
+  - `bbenoist.Nix` -> `bbenoist.nix`
+  - `CoenraadS.bracket-pair-colorizer` -> `coenraads.bracket-pair-colorizer`
+  - `golang.Go` -> `golang.go`
+
+- `services.uptimed` now uses `/var/lib/uptimed` as its stateDirectory instead of `/var/spool/uptimed`. Make sure to move all files to the new directory.
+
 ## Other Notable Changes {#sec-release-21.11-notable-changes}
 
 - The setting [`services.openssh.logLevel`](options.html#opt-services.openssh.logLevel) `"VERBOSE"` `"INFO"`. This brings NixOS in line with upstream and other Linux distributions, and reduces log spam on servers due to bruteforcing botnets.
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 768f1dc2a17..c703484666d 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -130,9 +130,12 @@ rec {
 
       nodeHostNames = map (c: c.config.system.name) (lib.attrValues nodes);
 
+      # TODO: This is an implementation error and needs fixing
+      # the testing famework cannot legitimately restrict hostnames further
+      # beyond RFC1035
       invalidNodeNames = lib.filter
         (node: builtins.match "^[A-z_]([A-z0-9_]+)?$" node == null)
-        (builtins.attrNames nodes);
+        nodeHostNames;
 
       testScript' =
         # Call the test script with the computed nodes.
@@ -146,7 +149,9 @@ rec {
         Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
         All machines are referenced as python variables in the testing framework which will break the
         script when special characters are used.
-        Please stick to alphanumeric chars and underscores as separation.
+
+        This is an IMPLEMENTATION ERROR and needs to be fixed. Meanwhile,
+        please stick to alphanumeric chars and underscores as separation.
       ''
     else lib.warnIf skipLint "Linting is disabled" (runCommand testDriverName
       {
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
index 3e88a4c20ad..a4e4fa8d0ed 100644
--- a/nixos/modules/hardware/all-firmware.nix
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -35,6 +35,14 @@ in {
       '';
     };
 
+    hardware.wirelessRegulatoryDatabase = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Load the wireless regulatory database at boot.
+      '';
+    };
+
   };
 
 
@@ -50,6 +58,7 @@ in {
         rtl8723bs-firmware
         rtl8761b-firmware
         rtw88-firmware
+        rtw89-firmware
         zd1211fw
         alsa-firmware
         sof-firmware
@@ -58,6 +67,7 @@ in {
         ++ optionals (versionOlder config.boot.kernelPackages.kernel.version "4.13") [
         rtl8723bs-firmware
       ];
+      hardware.wirelessRegulatoryDatabase = true;
     })
     (mkIf cfg.enableAllFirmware {
       assertions = [{
@@ -75,5 +85,8 @@ in {
         b43FirmwareCutter
       ] ++ optional (pkgs.stdenv.hostPlatform.isi686 || pkgs.stdenv.hostPlatform.isx86_64) facetimehd-firmware;
     })
+    (mkIf cfg.wirelessRegulatoryDatabase {
+      hardware.firmware = [ pkgs.wireless-regdb ];
+    })
   ];
 }
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index d94af0b5bf7..f0384513247 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -654,7 +654,11 @@ in
       ];
 
     fileSystems."/" =
-      { fsType = "tmpfs";
+      # This module is often over-layed onto an existing host config
+      # that defines `/`. We use mkOverride 60 to override standard
+      # values, but at the same time leave room for mkForce values
+      # targeted at the image build.
+      { fsType = mkOverride 60 "tmpfs";
         options = [ "mode=0755" ];
       };
 
diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix
index 238ab6d0617..f7543fdf4a2 100644
--- a/nixos/modules/installer/netboot/netboot.nix
+++ b/nixos/modules/installer/netboot/netboot.nix
@@ -30,7 +30,11 @@ with lib;
           else [ pkgs.grub2 pkgs.syslinux ]);
 
     fileSystems."/" =
-      { fsType = "tmpfs";
+      # This module is often over-layed onto an existing host config
+      # that defines `/`. We use mkOverride 60 to override standard
+      # values, but at the same time leave room for mkForce values
+      # targeted at the image build.
+      { fsType = mkOverride 60 "tmpfs";
         options = [ "mode=0755" ];
       };
 
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index e3576074a5b..15c76287e34 100644
--- a/nixos/modules/installer/tools/nix-fallback-paths.nix
+++ b/nixos/modules/installer/tools/nix-fallback-paths.nix
@@ -1,7 +1,7 @@
 {
-  x86_64-linux = "/nix/store/qsgz2hhn6mzlzp53a7pwf9z2pq3l5z6h-nix-2.3.14";
-  i686-linux = "/nix/store/1yw40bj04lykisw2jilq06lir3k9ga4a-nix-2.3.14";
-  aarch64-linux = "/nix/store/32yzwmynmjxfrkb6y6l55liaqdrgkj4a-nix-2.3.14";
-  x86_64-darwin = "/nix/store/06j0vi2d13w4l0p3jsigq7lk4x6gkycj-nix-2.3.14";
-  aarch64-darwin = "/nix/store/77wi7vpbrghw5rgws25w30bwb8yggnk9-nix-2.3.14";
+  x86_64-linux = "/nix/store/jhbxh1jwjc3hjhzs9y2hifdn0rmnfwaj-nix-2.3.15";
+  i686-linux = "/nix/store/9pspwnkdrgzma1l4xlv7arhwa56y16di-nix-2.3.15";
+  aarch64-linux = "/nix/store/72aqi5g7f4fhgvgafbcqwcpqjgnczj48-nix-2.3.15";
+  x86_64-darwin = "/nix/store/6p6qwp73dgfkqhynmxrzbx1lcfgfpqal-nix-2.3.15";
+  aarch64-darwin = "/nix/store/dmq2vksdhssgfl822shd0ky3x5x0klh4-nix-2.3.15";
 }
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 858c7ee53db..a7a31ba8576 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -187,6 +187,7 @@ in
       #seeks = 148; # removed 2020-06-21
       prosody = 149;
       i2pd = 150;
+      systemd-coredump = 151;
       systemd-network = 152;
       systemd-resolve = 153;
       systemd-timesync = 154;
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index a6fffb76f6e..8f246a9278b 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -103,9 +103,10 @@ in
       ''
         NAME=NixOS
         ID=nixos
-        VERSION="${cfg.version} (${cfg.codeName})"
+        VERSION="${cfg.release} (${cfg.codeName})"
         VERSION_CODENAME=${toLower cfg.codeName}
-        VERSION_ID="${cfg.version}"
+        VERSION_ID="${cfg.release}"
+        BUILD_ID="${cfg.version}"
         PRETTY_NAME="NixOS ${cfg.release} (${cfg.codeName})"
         LOGO="nix-snowflake"
         HOME_URL="https://nixos.org/"
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 4d1700ed99a..1a6d8599e7e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -236,6 +236,7 @@
   ./security/doas.nix
   ./security/systemd-confinement.nix
   ./security/tpm2.nix
+  ./services/admin/meshcentral.nix
   ./services/admin/oxidized.nix
   ./services/admin/salt/master.nix
   ./services/admin/salt/minion.nix
@@ -519,6 +520,7 @@
   ./services/misc/logkeys.nix
   ./services/misc/leaps.nix
   ./services/misc/lidarr.nix
+  ./services/misc/libreddit.nix
   ./services/misc/lifecycled.nix
   ./services/misc/mame.nix
   ./services/misc/matrix-appservice-discord.nix
@@ -529,6 +531,7 @@
   ./services/misc/mediatomb.nix
   ./services/misc/metabase.nix
   ./services/misc/mwlib.nix
+  ./services/misc/mx-puppet-discord.nix
   ./services/misc/n8n.nix
   ./services/misc/nix-daemon.nix
   ./services/misc/nix-gc.nix
@@ -633,6 +636,7 @@
   ./services/network-filesystems/glusterfs.nix
   ./services/network-filesystems/kbfs.nix
   ./services/network-filesystems/ipfs.nix
+  ./services/network-filesystems/litestream/default.nix
   ./services/network-filesystems/netatalk.nix
   ./services/network-filesystems/nfsd.nix
   ./services/network-filesystems/openafs/client.nix
@@ -960,6 +964,7 @@
   ./services/web-apps/moodle.nix
   ./services/web-apps/nextcloud.nix
   ./services/web-apps/nexus.nix
+  ./services/web-apps/node-red.nix
   ./services/web-apps/plantuml-server.nix
   ./services/web-apps/plausible.nix
   ./services/web-apps/pgpkeyserver-lite.nix
diff --git a/nixos/modules/programs/captive-browser.nix b/nixos/modules/programs/captive-browser.nix
index 1f223e2475c..007b0369ec1 100644
--- a/nixos/modules/programs/captive-browser.nix
+++ b/nixos/modules/programs/captive-browser.nix
@@ -27,6 +27,7 @@ in
       browser = mkOption {
         type = types.str;
         default = concatStringsSep " " [
+          ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"''
           ''${pkgs.chromium}/bin/chromium''
           ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
           ''--proxy-server="socks5://$PROXY"''
@@ -111,6 +112,7 @@ in
     security.wrappers.captive-browser = {
       capabilities = "cap_net_raw+p";
       source = pkgs.writeShellScript "captive-browser" ''
+        export PREV_CONFIG_HOME="$XDG_CONFIG_HOME"
         export XDG_CONFIG_HOME=${pkgs.writeTextDir "captive-browser.toml" ''
                                   browser = """${cfg.browser}"""
                                   dhcp-dns = """${cfg.dhcp-dns}"""
diff --git a/nixos/modules/services/admin/meshcentral.nix b/nixos/modules/services/admin/meshcentral.nix
new file mode 100644
index 00000000000..ae7b6edda7d
--- /dev/null
+++ b/nixos/modules/services/admin/meshcentral.nix
@@ -0,0 +1,53 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.meshcentral;
+  configFormat = pkgs.formats.json {};
+  configFile = configFormat.generate "meshcentral-config.json" cfg.settings;
+in with lib; {
+  options.services.meshcentral = with types; {
+    enable = mkEnableOption "MeshCentral computer management server";
+    package = mkOption {
+      description = "MeshCentral package to use. Replacing this may be necessary to add dependencies for extra functionality.";
+      type = types.package;
+      default = pkgs.meshcentral;
+      defaultText = "pkgs.meshcentral";
+    };
+    settings = mkOption {
+      description = ''
+        Settings for MeshCentral. Refer to upstream documentation for details:
+
+        <itemizedlist>
+          <listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json">JSON Schema definition</link></para></listitem>
+          <listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/sample-config.json">simple sample configuration</link></para></listitem>
+          <listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/sample-config-advanced.json">complex sample configuration</link></para></listitem>
+          <listitem><para><link xlink:href="https://www.meshcommander.com/meshcentral2">Old homepage) with documentation link</link></para></listitem>
+        </itemizedlist>
+      '';
+      type = types.submodule {
+        freeformType = configFormat.type;
+      };
+      example = {
+        settings = {
+          WANonly = true;
+          Cert = "meshcentral.example.com";
+          TlsOffload = "10.0.0.2,fd42::2";
+          Port = 4430;
+        };
+        domains."".certUrl = "https://meshcentral.example.com/";
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    services.meshcentral.settings.settings.autoBackup.backupPath = lib.mkDefault "/var/lib/meshcentral/backups";
+    systemd.services.meshcentral = {
+      wantedBy = ["multi-user.target"];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/meshcentral --datapath /var/lib/meshcentral --configfile ${configFile}";
+        DynamicUser = true;
+        StateDirectory = "meshcentral";
+        CacheDirectory = "meshcentral";
+      };
+    };
+  };
+  meta.maintainers = [ maintainers.lheckemann ];
+}
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index 24d86628b21..8f937a13231 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -60,6 +60,45 @@ in {
         sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
       };
     };
+
+    corefile = mkOption {
+      description = ''
+        Custom coredns corefile configuration.
+
+        See: <link xlink:href="https://coredns.io/manual/toc/#configuration"/>.
+      '';
+      type = types.str;
+      default = ''
+        .:${toString ports.dns} {
+          errors
+          health :${toString ports.health}
+          kubernetes ${cfg.clusterDomain} in-addr.arpa ip6.arpa {
+            pods insecure
+            fallthrough in-addr.arpa ip6.arpa
+          }
+          prometheus :${toString ports.metrics}
+          forward . /etc/resolv.conf
+          cache 30
+          loop
+          reload
+          loadbalance
+        }'';
+      defaultText = ''
+        .:${toString ports.dns} {
+          errors
+          health :${toString ports.health}
+          kubernetes ''${config.services.kubernetes.addons.dns.clusterDomain} in-addr.arpa ip6.arpa {
+            pods insecure
+            fallthrough in-addr.arpa ip6.arpa
+          }
+          prometheus :${toString ports.metrics}
+          forward . /etc/resolv.conf
+          cache 30
+          loop
+          reload
+          loadbalance
+        }'';
+    };
   };
 
   config = mkIf cfg.enable {
@@ -151,20 +190,7 @@ in {
           namespace = "kube-system";
         };
         data = {
-          Corefile = ".:${toString ports.dns} {
-            errors
-            health :${toString ports.health}
-            kubernetes ${cfg.clusterDomain} in-addr.arpa ip6.arpa {
-              pods insecure
-              fallthrough in-addr.arpa ip6.arpa
-            }
-            prometheus :${toString ports.metrics}
-            forward . /etc/resolv.conf
-            cache 30
-            loop
-            reload
-            loadbalance
-          }";
+          Corefile = cfg.corefile;
         };
       };
 
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index d9311d3e3a0..faf951d8157 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -189,7 +189,7 @@ in
         # manually paste it in place. Just symlink.
         # otherwise, create the target file, ready for users to insert the token
 
-        mkdir -p $(dirname ${certmgrAPITokenPath})
+        mkdir -p "$(dirname "${certmgrAPITokenPath}")"
         if [ -f "${cfsslAPITokenPath}" ]; then
           ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}"
         else
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index 2c6d9530a6b..15c37c2bc76 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -339,6 +339,9 @@ in
               <literal>CI_SERVER_URL=&lt;CI server URL&gt;</literal>
 
               <literal>REGISTRATION_TOKEN=&lt;registration secret&gt;</literal>
+
+              WARNING: make sure to use quoted absolute path,
+              or it is going to be copied to Nix Store.
             '';
           };
           registrationFlags = mkOption {
@@ -523,7 +526,10 @@ in
     };
   };
   config = mkIf cfg.enable {
-    warnings = optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
+    warnings = (mapAttrsToList
+      (n: v: "services.gitlab-runner.services.${n}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
+      (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services))
+    ++ optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
     environment.systemPackages = [ cfg.package ];
     systemd.services.gitlab-runner = {
       description = "Gitlab Runner";
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 9c0740f28c9..8873f6d00e0 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -272,7 +272,7 @@ in {
       }
       (mkIf (cfg.bind != null) { bind = cfg.bind; })
       (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; unixsocketperm = "${toString cfg.unixSocketPerm}"; })
-      (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${cfg.slaveOf.port}"; })
+      (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${toString cfg.slaveOf.port}"; })
       (mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
       (mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
     ];
diff --git a/nixos/modules/services/hardware/sane.nix b/nixos/modules/services/hardware/sane.nix
index 8c1bde7b415..ccf726bd182 100644
--- a/nixos/modules/services/hardware/sane.nix
+++ b/nixos/modules/services/hardware/sane.nix
@@ -4,7 +4,10 @@ with lib;
 
 let
 
-  pkg = pkgs.sane-backends;
+  pkg = pkgs.sane-backends.override {
+    scanSnapDriversUnfree = config.hardware.sane.drivers.scanSnap.enable;
+    scanSnapDriversPackage = config.hardware.sane.drivers.scanSnap.package;
+  };
 
   sanedConf = pkgs.writeTextFile {
     name = "saned.conf";
@@ -98,6 +101,28 @@ in
       '';
     };
 
+    hardware.sane.drivers.scanSnap.enable = mkOption {
+      type = types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable drivers for the Fujitsu ScanSnap scanners.
+
+        The driver files are unfree and extracted from the Windows driver image.
+      '';
+    };
+
+    hardware.sane.drivers.scanSnap.package = mkOption {
+      type = types.package;
+      default = pkgs.sane-drivers.epjitsu;
+      description = ''
+        Epjitsu driver package to use. Useful if you want to extract the driver files yourself.
+
+        The process is described in the <literal>/etc/sane.d/epjitsu.conf</literal> file in
+        the <literal>sane-backends</literal> package.
+      '';
+    };
+
     services.saned.enable = mkOption {
       type = types.bool;
       default = false;
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index b6c1ca3e61a..2f8e595cad0 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -523,19 +523,12 @@ in
         ''}
 
         # update all hooks' binary paths
-        HOOKS=$(find ${cfg.repositoryRoot} -mindepth 4 -maxdepth 6 -type f -wholename "*git/hooks/*")
-        if [ "$HOOKS" ]
-        then
-          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gitea,${gitea}/bin/gitea,g' $HOOKS
-          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/env,${pkgs.coreutils}/bin/env,g' $HOOKS
-          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/bash,${pkgs.bash}/bin/bash,g' $HOOKS
-          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/perl,${pkgs.perl}/bin/perl,g' $HOOKS
-        fi
+        ${gitea}/bin/gitea admin regenerate hooks
 
         # update command option in authorized_keys
         if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
         then
-          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gitea,${gitea}/bin/gitea,g' ${cfg.stateDir}/.ssh/authorized_keys
+          ${gitea}/bin/gitea admin regenerate keys
         fi
       '';
 
diff --git a/nixos/modules/services/misc/libreddit.nix b/nixos/modules/services/misc/libreddit.nix
new file mode 100644
index 00000000000..77b34a85620
--- /dev/null
+++ b/nixos/modules/services/misc/libreddit.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+  let
+    cfg = config.services.libreddit;
+
+    args = concatStringsSep " " ([
+      "--port ${toString cfg.port}"
+      "--address ${cfg.address}"
+    ] ++ optional cfg.redirect "--redirect-https");
+
+in
+{
+  options = {
+    services.libreddit = {
+      enable = mkEnableOption "Private front-end for Reddit";
+
+      address = mkOption {
+        default = "0.0.0.0";
+        example = "127.0.0.1";
+        type =  types.str;
+        description = "The address to listen on";
+      };
+
+      port = mkOption {
+        default = 8080;
+        example = 8000;
+        type = types.port;
+        description = "The port to listen on";
+      };
+
+      redirect = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the redirecting to HTTPS";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the libreddit web interface";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.libreddit = {
+        description = "Private front-end for Reddit";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          ExecStart = "${pkgs.libreddit}/bin/libreddit ${args}";
+          AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+          Restart = "on-failure";
+          RestartSec = "2s";
+        };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mx-puppet-discord.nix b/nixos/modules/services/misc/mx-puppet-discord.nix
new file mode 100644
index 00000000000..11116f7c348
--- /dev/null
+++ b/nixos/modules/services/misc/mx-puppet-discord.nix
@@ -0,0 +1,120 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/mx-puppet-discord";
+  registrationFile = "${dataDir}/discord-registration.yaml";
+  cfg = config.services.mx-puppet-discord;
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "mx-puppet-discord-config.json" cfg.settings;
+
+in {
+  options = {
+    services.mx-puppet-discord = {
+      enable = mkEnableOption ''
+        mx-puppet-discord is a discord puppeting bridge for matrix.
+        It handles bridging private and group DMs, as well as Guilds (servers)
+      '';
+
+      settings = mkOption rec {
+        apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
+        default = {
+          bridge.port = 8434;
+          presence = {
+            enabled = true;
+            interval = 500;
+          };
+          provisioning.whitelist = [ ];
+          relay.whitelist = [ ];
+
+          # variables are preceded by a colon.
+          namePatterns = {
+            user = ":name";
+            userOverride = ":displayname";
+            room = ":name";
+            group = ":name";
+          };
+
+          #defaults to sqlite but can be configured to use postgresql with
+          #connstring
+          database.filename = "${dataDir}/mx-puppet-discord/database.db";
+          logging = {
+            console = "info";
+            lineDateFormat = "MMM-D HH:mm:ss.SSS";
+          };
+        };
+        example = literalExample ''
+          {
+            bridge = {
+              bindAddress = "localhost";
+              domain = "example.com";
+              homeserverUrl = "https://example.com";
+            };
+
+            provisioning.whitelist = [ "@admin:example.com" ];
+            relay.whitelist = [ "@.*:example.com" ];
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/matrix-discord/mx-puppet-discord/blob/master/sample.config.yaml">
+          sample.config.yaml</link>.
+        '';
+      };
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        description = ''
+          List of Systemd services to require and wait for when starting the application service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mx-puppet-discord = {
+      description = ''
+        mx-puppet-discord is a discord puppeting bridge for matrix.
+        It handles bridging private and group DMs, as well as Guilds (servers).
+      '';
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        # generate the appservice's registration file if absent
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord -r -c ${settingsFile} \
+          -f ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = pkgs.mx-puppet-discord;
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+
+        ExecStart = ''
+          ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord -c ${settingsFile}
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ govanify ];
+}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 133e96da0ec..70b27b7d3d0 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -458,7 +458,7 @@ in
                 description = "The flake reference to which <option>from></option> is to be rewritten.";
               };
               flake = mkOption {
-                type = types.unspecified;
+                type = types.nullOr types.attrs;
                 default = null;
                 example = literalExample "nixpkgs";
                 description = ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index d648de6a414..9182c2f2ed8 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -33,6 +33,7 @@ let
     "domain"
     "dovecot"
     "fritzbox"
+    "influxdb"
     "json"
     "jitsi"
     "kea"
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix b/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix
new file mode 100644
index 00000000000..ba45173e946
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.influxdb;
+in
+{
+  port = 9122;
+  extraOpts = {
+    sampleExpiry = mkOption {
+      type = types.str;
+      default = "5m";
+      example = "10m";
+      description = "How long a sample is valid for";
+    };
+    udpBindAddress = mkOption {
+      type = types.str;
+      default = ":9122";
+      example = "192.0.2.1:9122";
+      description = "Address on which to listen for udp packets";
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      RuntimeDirectory = "prometheus-influxdb-exporter";
+      ExecStart = ''
+        ${pkgs.prometheus-influxdb-exporter}/bin/influxdb_exporter \
+        --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+        --influxdb.sample-expiry ${cfg.sampleExpiry} ${concatStringsSep " " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/litestream/default.nix b/nixos/modules/services/network-filesystems/litestream/default.nix
new file mode 100644
index 00000000000..f1806c5af0a
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/litestream/default.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.litestream;
+  settingsFormat = pkgs.formats.yaml {};
+in
+{
+  options.services.litestream = {
+    enable = mkEnableOption "litestream";
+
+    package = mkOption {
+      description = "Package to use.";
+      default = pkgs.litestream;
+      defaultText = "pkgs.litestream";
+      type = types.package;
+    };
+
+    settings = mkOption {
+      description = ''
+        See the <link xlink:href="https://litestream.io/reference/config/">documentation</link>.
+      '';
+      type = settingsFormat.type;
+      example = {
+        dbs = [
+          {
+            path = "/var/lib/db1";
+            replicas = [
+              {
+                url = "s3://mybkt.litestream.io/db1";
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/secrets/litestream";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        By default, Litestream will perform environment variable expansion
+        within the config file before reading it. Any references to ''$VAR or
+        ''${VAR} formatted variables will be replaced with their environment
+        variable values. If no value is set then it will be replaced with an
+        empty string.
+
+        <programlisting>
+          # Content of the environment file
+          LITESTREAM_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxxxxx
+          LITESTREAM_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxx
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        this exporter is running.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    environment.etc = {
+      "litestream.yml" = {
+        source = settingsFormat.generate "litestream-config.yaml" cfg.settings;
+      };
+    };
+
+    systemd.services.litestream = {
+      description = "Litestream";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = {
+        EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${cfg.package}/bin/litestream replicate";
+        Restart = "always";
+        User = "litestream";
+        Group = "litestream";
+      };
+    };
+
+    users.users.litestream = {
+      description = "Litestream user";
+      group = "litestream";
+      isSystemUser = true;
+    };
+    users.groups.litestream = {};
+  };
+  meta.doc = ./litestream.xml;
+}
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml
new file mode 100644
index 00000000000..598f9be8cf6
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml
@@ -0,0 +1,65 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="module-services-litestream">
+ <title>Litestream</title>
+ <para>
+  <link xlink:href="https://litestream.io/">Litestream</link> is a standalone streaming
+  replication tool for SQLite.
+ </para>
+
+ <section xml:id="module-services-litestream-configuration">
+  <title>Configuration</title>
+
+  <para>
+   Litestream service is managed by a dedicated user named <literal>litestream</literal>
+   which needs permission to the database file. Here's an example config which gives
+   required permissions to access <link linkend="opt-services.grafana.database.path">
+   grafana database</link>:
+<programlisting>
+{ pkgs, ... }:
+{
+  users.users.litestream.extraGroups = [ "grafana" ];
+
+  systemd.services.grafana.serviceConfig.ExecStartPost = "+" + pkgs.writeShellScript "grant-grafana-permissions" ''
+    timeout=10
+
+    while [ ! -f /var/lib/grafana/data/grafana.db ];
+    do
+      if [ "$timeout" == 0 ]; then
+        echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
+        exit 1
+      fi
+
+      sleep 1
+
+      ((timeout--))
+    done
+
+    find /var/lib/grafana -type d -exec chmod -v 775 {} \;
+    find /var/lib/grafana -type f -exec chmod -v 660 {} \;
+  '';
+
+  services.litestream = {
+    enable = true;
+
+    environmentFile = "/run/secrets/litestream";
+
+    settings = {
+      dbs = [
+        {
+          path = "/var/lib/grafana/data/grafana.db";
+          replicas = [{
+            url = "s3://mybkt.litestream.io/grafana";
+          }];
+        }
+      ];
+    };
+  };
+}
+</programlisting>
+  </para>
+ </section>
+
+</chapter>
diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix
index 1923afdf83f..c14adbda3c5 100644
--- a/nixos/modules/services/networking/bird.nix
+++ b/nixos/modules/services/networking/bird.nix
@@ -10,8 +10,8 @@ let
       birdBin = if variant == "bird6" then "bird6" else "bird";
       birdc = if variant == "bird6" then "birdc6" else "birdc";
       descr =
-        { bird = "1.9.x with IPv4 suport";
-          bird6 = "1.9.x with IPv6 suport";
+        { bird = "1.6.x with IPv4 support";
+          bird6 = "1.6.x with IPv6 support";
           bird2 = "2.x";
         }.${variant};
     in {
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index cdc3a172ea7..f982621e232 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -339,6 +339,8 @@ in
         description =
           ''
             Whether to log rejected or dropped incoming connections.
+            Note: The logs are found in the kernel logs, i.e. dmesg
+            or journalctl -k.
           '';
       };
 
@@ -350,6 +352,8 @@ in
             Whether to log all rejected or dropped incoming packets.
             This tends to give a lot of log messages, so it's mostly
             useful for debugging.
+            Note: The logs are found in the kernel logs, i.e. dmesg
+            or journalctl -k.
           '';
       };
 
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
index 72773b83a49..b11402204ae 100644
--- a/nixos/modules/services/networking/kea.nix
+++ b/nixos/modules/services/networking/kea.nix
@@ -238,6 +238,10 @@ in
         KEA_PIDFILE_DIR = "/run/kea";
       };
 
+      restartTriggers = [
+        ctrlAgentConfig
+      ];
+
       serviceConfig = {
         ExecStart = "${package}/bin/kea-ctrl-agent -c /etc/kea/ctrl-agent.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
         KillMode = "process";
@@ -269,6 +273,10 @@ in
         KEA_PIDFILE_DIR = "/run/kea";
       };
 
+      restartTriggers = [
+        dhcp4Config
+      ];
+
       serviceConfig = {
         ExecStart = "${package}/bin/kea-dhcp4 -c /etc/kea/dhcp4-server.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
         # Kea does not request capabilities by itself
@@ -307,6 +315,10 @@ in
         KEA_PIDFILE_DIR = "/run/kea";
       };
 
+      restartTriggers = [
+        dhcp6Config
+      ];
+
       serviceConfig = {
         ExecStart = "${package}/bin/kea-dhcp6 -c /etc/kea/dhcp6-server.conf ${lib.escapeShellArgs cfg.dhcp6.extraArgs}";
         # Kea does not request capabilities by itself
@@ -343,6 +355,10 @@ in
         KEA_PIDFILE_DIR = "/run/kea";
       };
 
+      restartTriggers = [
+        dhcpDdnsConfig
+      ];
+
       serviceConfig = {
         ExecStart = "${package}/bin/kea-dhcp-ddns -c /etc/kea/dhcp-ddns.conf ${lib.escapeShellArgs cfg.dhcp-ddns.extraArgs}";
         AmbientCapabilites = [
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 064018057cd..c8861171dd6 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -6,7 +6,6 @@ let
   cfg = config.networking.networkmanager;
 
   basePackages = with pkgs; [
-    crda
     modemmanager
     networkmanager
     networkmanager-fortisslvpn
@@ -49,6 +48,7 @@ let
       rc-manager =
         if config.networking.resolvconf.enable then "resolvconf"
         else "unmanaged";
+      firewall-backend = cfg.firewallBackend;
     })
     (mkSection "keyfile" {
       unmanaged-devices =
@@ -244,6 +244,15 @@ in {
         '';
       };
 
+      firewallBackend = mkOption {
+        type = types.enum [ "iptables" "nftables" "none" ];
+        default = "iptables";
+        description = ''
+          Which firewall backend should be used for configuring masquerading with shared mode.
+          If set to none, NetworkManager doesn't manage the configuration at all.
+        '';
+      };
+
       logLevel = mkOption {
         type = types.enum [ "OFF" "ERR" "WARN" "INFO" "DEBUG" "TRACE" ];
         default = "WARN";
@@ -404,6 +413,8 @@ in {
       }
     ];
 
+    hardware.wirelessRegulatoryDatabase = true;
+
     environment.etc = with pkgs; {
       "NetworkManager/NetworkManager.conf".source = configFile;
 
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index cb75142965e..72f37c32253 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -103,6 +103,7 @@ in
     }];
     boot.blacklistedKernelModules = [ "ip_tables" ];
     environment.systemPackages = [ pkgs.nftables ];
+    networking.networkmanager.firewallBackend = mkDefault "nftables";
     systemd.services.nftables = {
       description = "nftables firewall";
       before = [ "network-pre.target" ];
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index 28348c7893a..2e92fe51e90 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -5,15 +5,16 @@ with lib;
 let
   cfg = config.services.syncthing;
   defaultUser = "syncthing";
+  defaultGroup = defaultUser;
 
   devices = mapAttrsToList (name: device: {
     deviceID = device.id;
     inherit (device) name addresses introducer;
-  }) cfg.declarative.devices;
+  }) cfg.devices;
 
   folders = mapAttrsToList ( _: folder: {
     inherit (folder) path id label type;
-    devices = map (device: { deviceId = cfg.declarative.devices.${device}.id; }) folder.devices;
+    devices = map (device: { deviceId = cfg.devices.${device}.id; }) folder.devices;
     rescanIntervalS = folder.rescanInterval;
     fsWatcherEnabled = folder.watch;
     fsWatcherDelayS = folder.watchDelay;
@@ -23,215 +24,218 @@ let
   }) (filterAttrs (
     _: folder:
     folder.enable
-  ) cfg.declarative.folders);
-
-  # get the api key by parsing the config.xml
-  getApiKey = pkgs.writers.writeDash "getAPIKey" ''
-    ${pkgs.libxml2}/bin/xmllint \
-      --xpath 'string(configuration/gui/apikey)'\
-      ${cfg.configDir}/config.xml
-  '';
+  ) cfg.folders);
 
   updateConfig = pkgs.writers.writeDash "merge-syncthing-config" ''
     set -efu
-    # wait for syncthing port to open
-    until ${pkgs.curl}/bin/curl -Ss ${cfg.guiAddress} -o /dev/null; do
-      sleep 1
-    done
-
-    API_KEY=$(${getApiKey})
-    OLD_CFG=$(${pkgs.curl}/bin/curl -Ss \
-      -H "X-API-Key: $API_KEY" \
-      ${cfg.guiAddress}/rest/system/config)
-
-    # generate the new config by merging with the nixos config options
-    NEW_CFG=$(echo "$OLD_CFG" | ${pkgs.jq}/bin/jq -s '.[] as $in | $in * {
-      "devices": (${builtins.toJSON devices}${optionalString (! cfg.declarative.overrideDevices) " + $in.devices"}),
-      "folders": (${builtins.toJSON folders}${optionalString (! cfg.declarative.overrideFolders) " + $in.folders"})
-    }')
-
-    # POST the new config to syncthing
-    echo "$NEW_CFG" | ${pkgs.curl}/bin/curl -Ss \
-      -H "X-API-Key: $API_KEY" \
-      ${cfg.guiAddress}/rest/system/config -d @-
-
-    # restart syncthing after sending the new config
-    ${pkgs.curl}/bin/curl -Ss \
-      -H "X-API-Key: $API_KEY" \
-      -X POST \
-      ${cfg.guiAddress}/rest/system/restart
+
+    # get the api key by parsing the config.xml
+    while
+        ! api_key=$(${pkgs.libxml2}/bin/xmllint \
+            --xpath 'string(configuration/gui/apikey)' \
+            ${cfg.configDir}/config.xml)
+    do sleep 1; done
+
+    curl() {
+        ${pkgs.curl}/bin/curl -sS -H "X-API-Key: $api_key" \
+            --retry 1000 --retry-delay 1 --retry-all-errors \
+            "$@"
+    }
+
+    # query the old config
+    old_cfg=$(curl ${cfg.guiAddress}/rest/config)
+
+    # generate the new config by merging with the NixOS config options
+    new_cfg=$(echo "$old_cfg" | ${pkgs.jq}/bin/jq -c '. * {
+        "devices": (${builtins.toJSON devices}${optionalString (! cfg.overrideDevices) " + .devices"}),
+        "folders": (${builtins.toJSON folders}${optionalString (! cfg.overrideFolders) " + .folders"})
+    } * ${builtins.toJSON cfg.extraOptions}')
+
+    # send the new config
+    curl -X PUT -d "$new_cfg" ${cfg.guiAddress}/rest/config
+
+    # restart Syncthing if required
+    if curl ${cfg.guiAddress}/rest/config/restart-required |
+       ${pkgs.jq}/bin/jq -e .requiresRestart > /dev/null; then
+        curl -X POST ${cfg.guiAddress}/rest/system/restart
+    fi
   '';
 in {
   ###### interface
   options = {
     services.syncthing = {
 
-      enable = mkEnableOption ''
-        Syncthing - the self-hosted open-source alternative
-        to Dropbox and Bittorrent Sync. Initial interface will be
-        available on http://127.0.0.1:8384/.
-      '';
-
-      declarative = {
-        cert = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          description = ''
-            Path to users cert.pem file, will be copied into the syncthing's
-            <literal>configDir</literal>
-          '';
-        };
+      enable = mkEnableOption
+        "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
 
-        key = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          description = ''
-            Path to users key.pem file, will be copied into the syncthing's
-            <literal>configDir</literal>
-          '';
-        };
+      cert = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the <literal>cert.pem</literal> file, which will be copied into Syncthing's
+          <link linkend="opt-services.syncthing.configDir">configDir</link>.
+        '';
+      };
 
-        overrideDevices = mkOption {
-          type = types.bool;
-          default = true;
-          description = ''
-            Whether to delete the devices which are not configured via the
-            <literal>declarative.devices</literal> option.
-            If set to false, devices added via the webinterface will
-            persist but will have to be deleted manually.
-          '';
+      key = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the <literal>key.pem</literal> file, which will be copied into Syncthing's
+          <link linkend="opt-services.syncthing.configDir">configDir</link>.
+        '';
+      };
+
+      overrideDevices = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to delete the devices which are not configured via the
+          <link linkend="opt-services.syncthing.devices">devices</link> option.
+          If set to <literal>false</literal>, devices added via the web
+          interface will persist and will have to be deleted manually.
+        '';
+      };
+
+      devices = mkOption {
+        default = {};
+        description = ''
+          Peers/devices which Syncthing should communicate with.
+
+          Note that you can still add devices manually, but those changes
+          will be reverted on restart if <link linkend="opt-services.syncthing.overrideDevices">overrideDevices</link>
+          is enabled.
+        '';
+        example = {
+          bigbox = {
+            id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
+            addresses = [ "tcp://192.168.0.10:51820" ];
+          };
         };
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          options = {
+
+            name = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The name of the device.
+              '';
+            };
 
-        devices = mkOption {
-          default = {};
-          description = ''
-            Peers/devices which syncthing should communicate with.
-          '';
-          example = {
-            bigbox = {
-              id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
-              addresses = [ "tcp://192.168.0.10:51820" ];
+            addresses = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                The addresses used to connect to the device.
+                If this is left empty, dynamic configuration is attempted.
+              '';
+            };
+
+            id = mkOption {
+              type = types.str;
+              description = ''
+                The device ID. See <link xlink:href="https://docs.syncthing.net/dev/device-ids.html"/>.
+              '';
             };
+
+            introducer = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether the device should act as an introducer and be allowed
+                to add folders on this computer.
+                See <link xlink:href="https://docs.syncthing.net/users/introducer.html"/>.
+              '';
+            };
+
           };
-          type = types.attrsOf (types.submodule ({ name, ... }: {
-            options = {
-
-              name = mkOption {
-                type = types.str;
-                default = name;
-                description = ''
-                  Name of the device
-                '';
-              };
-
-              addresses = mkOption {
-                type = types.listOf types.str;
-                default = [];
-                description = ''
-                  The addresses used to connect to the device.
-                  If this is let empty, dynamic configuration is attempted
-                '';
-              };
-
-              id = mkOption {
-                type = types.str;
-                description = ''
-                  The id of the other peer, this is mandatory. It's documented at
-                  https://docs.syncthing.net/dev/device-ids.html
-                '';
-              };
-
-              introducer = mkOption {
-                type = types.bool;
-                default = false;
-                description = ''
-                  If the device should act as an introducer and be allowed
-                  to add folders on this computer.
-                '';
-              };
+        }));
+      };
+
+      overrideFolders = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to delete the folders which are not configured via the
+          <link linkend="opt-services.syncthing.folders">folders</link> option.
+          If set to <literal>false</literal>, folders added via the web
+          interface will persist and will have to be deleted manually.
+        '';
+      };
 
+      folders = mkOption {
+        default = {};
+        description = ''
+          Folders which should be shared by Syncthing.
+
+          Note that you can still add devices manually, but those changes
+          will be reverted on restart if <link linkend="opt-services.syncthing.overrideDevices">overrideDevices</link>
+          is enabled.
+        '';
+        example = literalExample ''
+          {
+            "/home/user/sync" = {
+              id = "syncme";
+              devices = [ "bigbox" ];
+            };
+          }
+        '';
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          options = {
+
+            enable = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to share this folder.
+                This option is useful when you want to define all folders
+                in one place, but not every machine should share all folders.
+              '';
             };
-          }));
-        };
 
-        overrideFolders = mkOption {
-          type = types.bool;
-          default = true;
-          description = ''
-            Whether to delete the folders which are not configured via the
-            <literal>declarative.folders</literal> option.
-            If set to false, folders added via the webinterface will persist
-            but will have to be deleted manually.
-          '';
-        };
+            path = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The path to the folder which should be shared.
+              '';
+            };
 
-        folders = mkOption {
-          default = {};
-          description = ''
-            folders which should be shared by syncthing.
-          '';
-          example = literalExample ''
-            {
-              "/home/user/sync" = {
-                id = "syncme";
-                devices = [ "bigbox" ];
-              };
-            }
-          '';
-          type = types.attrsOf (types.submodule ({ name, ... }: {
-            options = {
-
-              enable = mkOption {
-                type = types.bool;
-                default = true;
-                description = ''
-                  share this folder.
-                  This option is useful when you want to define all folders
-                  in one place, but not every machine should share all folders.
-                '';
-              };
-
-              path = mkOption {
-                type = types.str;
-                default = name;
-                description = ''
-                  The path to the folder which should be shared.
-                '';
-              };
-
-              id = mkOption {
-                type = types.str;
-                default = name;
-                description = ''
-                  The id of the folder. Must be the same on all devices.
-                '';
-              };
-
-              label = mkOption {
-                type = types.str;
-                default = name;
-                description = ''
-                  The label of the folder.
-                '';
-              };
-
-              devices = mkOption {
-                type = types.listOf types.str;
-                default = [];
-                description = ''
-                  The devices this folder should be shared with. Must be defined
-                  in the <literal>declarative.devices</literal> attribute.
-                '';
-              };
-
-              versioning = mkOption {
-                default = null;
-                description = ''
-                  How to keep changed/deleted files with syncthing.
-                  There are 4 different types of versioning with different parameters.
-                  See https://docs.syncthing.net/users/versioning.html
-                '';
-                example = [
+            id = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The ID of the folder. Must be the same on all devices.
+              '';
+            };
+
+            label = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The label of the folder.
+              '';
+            };
+
+            devices = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                The devices this folder should be shared with. Each device must
+                be defined in the <link linkend="opt-services.syncthing.devices">devices</link> option.
+              '';
+            };
+
+            versioning = mkOption {
+              default = null;
+              description = ''
+                How to keep changed/deleted files with Syncthing.
+                There are 4 different types of versioning with different parameters.
+                See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+              '';
+              example = literalExample ''
+                [
                   {
                     versioning = {
                       type = "simple";
@@ -257,87 +261,99 @@ in {
                   {
                     versioning = {
                       type = "external";
-                      params.versionsPath = pkgs.writers.writeBash "backup" ''
+                      params.versionsPath = pkgs.writers.writeBash "backup" '''
                         folderpath="$1"
                         filepath="$2"
                         rm -rf "$folderpath/$filepath"
-                      '';
+                      ''';
                     };
                   }
-                ];
-                type = with types; nullOr (submodule {
-                  options = {
-                    type = mkOption {
-                      type = enum [ "external" "simple" "staggered" "trashcan" ];
-                      description = ''
-                        Type of versioning.
-                        See https://docs.syncthing.net/users/versioning.html
-                      '';
-                    };
-                    params = mkOption {
-                      type = attrsOf (either str path);
-                      description = ''
-                        Parameters for versioning. Structure depends on versioning.type.
-                        See https://docs.syncthing.net/users/versioning.html
-                      '';
-                    };
+                ]
+              '';
+              type = with types; nullOr (submodule {
+                options = {
+                  type = mkOption {
+                    type = enum [ "external" "simple" "staggered" "trashcan" ];
+                    description = ''
+                      The type of versioning.
+                      See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                    '';
+                  };
+                  params = mkOption {
+                    type = attrsOf (either str path);
+                    description = ''
+                      The parameters for versioning. Structure depends on
+                      <link linkend="opt-services.syncthing.folders._name_.versioning.type">versioning.type</link>.
+                      See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                    '';
                   };
-                });
-              };
-
-              rescanInterval = mkOption {
-                type = types.int;
-                default = 3600;
-                description = ''
-                  How often the folders should be rescaned for changes.
-                '';
-              };
-
-              type = mkOption {
-                type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
-                default = "sendreceive";
-                description = ''
-                  Whether to send only changes from this folder, only receive them
-                  or propagate both.
-                '';
-              };
-
-              watch = mkOption {
-                type = types.bool;
-                default = true;
-                description = ''
-                  Whether the folder should be watched for changes by inotify.
-                '';
-              };
-
-              watchDelay = mkOption {
-                type = types.int;
-                default = 10;
-                description = ''
-                  The delay after an inotify event is triggered.
-                '';
-              };
-
-              ignorePerms = mkOption {
-                type = types.bool;
-                default = true;
-                description = ''
-                  Whether to propagate permission changes.
-                '';
-              };
-
-              ignoreDelete = mkOption {
-                type = types.bool;
-                default = false;
-                description = ''
-                  Whether to delete files in destination. See <link
-                  xlink:href="https://docs.syncthing.net/advanced/folder-ignoredelete.html">
-                  upstream's docs</link>.
-                '';
-              };
+                };
+              });
+            };
+
+            rescanInterval = mkOption {
+              type = types.int;
+              default = 3600;
+              description = ''
+                How often the folder should be rescanned for changes.
+              '';
+            };
+
+            type = mkOption {
+              type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
+              default = "sendreceive";
+              description = ''
+                Whether to only send changes for this folder, only receive them
+                or both.
+              '';
+            };
+
+            watch = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether the folder should be watched for changes by inotify.
+              '';
+            };
+
+            watchDelay = mkOption {
+              type = types.int;
+              default = 10;
+              description = ''
+                The delay after an inotify event is triggered.
+              '';
+            };
+
+            ignorePerms = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to ignore permission changes.
+              '';
+            };
 
+            ignoreDelete = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether to skip deleting files that are deleted by peers.
+                See <link xlink:href="https://docs.syncthing.net/advanced/folder-ignoredelete.html"/>.
+              '';
             };
-          }));
+          };
+        }));
+      };
+
+      extraOptions = mkOption {
+        type = types.addCheck (pkgs.formats.json {}).type isAttrs;
+        default = {};
+        description = ''
+          Extra configuration options for Syncthing.
+          See <link xlink:href="https://docs.syncthing.net/users/config.html"/>.
+        '';
+        example = {
+          options.localAnnounceEnabled = false;
+          gui.theme = "black";
         };
       };
 
@@ -345,31 +361,35 @@ in {
         type = types.str;
         default = "127.0.0.1:8384";
         description = ''
-          Address to serve the GUI.
+          The address to serve the web interface at.
         '';
       };
 
       systemService = mkOption {
         type = types.bool;
         default = true;
-        description = "Auto launch Syncthing as a system service.";
+        description = ''
+          Whether to auto-launch Syncthing as a system service.
+        '';
       };
 
       user = mkOption {
         type = types.str;
         default = defaultUser;
+        example = "yourUser";
         description = ''
-          Syncthing will be run under this user (user will be created if it doesn't exist.
-          This can be your user name).
+          The user to run Syncthing as.
+          By default, a user named <literal>${defaultUser}</literal> will be created.
         '';
       };
 
       group = mkOption {
         type = types.str;
-        default = defaultUser;
+        default = defaultGroup;
+        example = "yourGroup";
         description = ''
-          Syncthing will be run under this group (group will not be created if it doesn't exist.
-          This can be your user name).
+          The group to run Syncthing under.
+          By default, a group named <literal>${defaultGroup}</literal> will be created.
         '';
       };
 
@@ -378,63 +398,67 @@ in {
         default = null;
         example = "socks5://address.com:1234";
         description = ''
-          Overwrites all_proxy environment variable for the syncthing process to
-          the given value. This is normaly used to let relay client connect
-          through SOCKS5 proxy server.
+          Overwrites the all_proxy environment variable for the Syncthing process to
+          the given value. This is normally used to let Syncthing connect
+          through a SOCKS5 proxy server.
+          See <link xlink:href="https://docs.syncthing.net/users/proxying.html"/>.
         '';
       };
 
       dataDir = mkOption {
         type = types.path;
         default = "/var/lib/syncthing";
+        example = "/home/yourUser";
         description = ''
-          Path where synced directories will exist.
+          The path where synchronised directories will exist.
         '';
       };
 
-      configDir = mkOption {
+      configDir = let
+        cond = versionAtLeast config.system.stateVersion "19.03";
+      in mkOption {
         type = types.path;
         description = ''
-          Path where the settings and keys will exist.
+          The path where the settings and keys will exist.
         '';
-        default =
-          let
-            nixos = config.system.stateVersion;
-            cond  = versionAtLeast nixos "19.03";
-          in cfg.dataDir + (optionalString cond "/.config/syncthing");
+        default = cfg.dataDir + (optionalString cond "/.config/syncthing");
+        defaultText = literalExample "dataDir${optionalString cond " + \"/.config/syncthing\""}";
       };
 
       openDefaultPorts = mkOption {
         type = types.bool;
         default = false;
-        example = literalExample "true";
+        example = true;
         description = ''
-          Open the default ports in the firewall:
-            - TCP 22000 for transfers
-            - UDP 21027 for discovery
-          If multiple users are running syncthing on this machine, you will need to manually open a set of ports for each instance and leave this disabled.
-          Alternatively, if are running only a single instance on this machine using the default ports, enable this.
+          Whether to open the default ports in the firewall: TCP 22000 for transfers
+          and UDP 21027 for discovery.
+
+          If multiple users are running Syncthing on this machine, you will need
+          to manually open a set of ports for each instance and leave this disabled.
+          Alternatively, if you are running only a single instance on this machine
+          using the default ports, enable this.
         '';
       };
 
       package = mkOption {
         type = types.package;
         default = pkgs.syncthing;
-        defaultText = "pkgs.syncthing";
-        example = literalExample "pkgs.syncthing";
+        defaultText = literalExample "pkgs.syncthing";
         description = ''
-          Syncthing package to use.
+          The Syncthing package to use.
         '';
       };
     };
   };
 
   imports = [
-    (mkRemovedOptionModule ["services" "syncthing" "useInotify"] ''
-      This option was removed because syncthing now has the inotify functionality included under the name "fswatcher".
-      It can be enabled on a per-folder basis through the webinterface.
+    (mkRemovedOptionModule [ "services" "syncthing" "useInotify" ] ''
+      This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher".
+      It can be enabled on a per-folder basis through the web interface.
     '')
-  ];
+  ] ++ map (o:
+    mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ]
+  ) [ "cert" "key" "devices" "folders" "overrideDevices" "overrideFolders" "extraOptions"];
 
   ###### implementation
 
@@ -457,8 +481,8 @@ in {
         };
     };
 
-    users.groups = mkIf (cfg.systemService && cfg.group == defaultUser) {
-      ${defaultUser}.gid =
+    users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) {
+      ${defaultGroup}.gid =
         config.ids.gids.syncthing;
     };
 
@@ -478,14 +502,14 @@ in {
           RestartForceExitStatus="3 4";
           User = cfg.user;
           Group = cfg.group;
-          ExecStartPre = mkIf (cfg.declarative.cert != null || cfg.declarative.key != null)
+          ExecStartPre = mkIf (cfg.cert != null || cfg.key != null)
             "+${pkgs.writers.writeBash "syncthing-copy-keys" ''
               install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir}
-              ${optionalString (cfg.declarative.cert != null) ''
-                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.declarative.cert} ${cfg.configDir}/cert.pem
+              ${optionalString (cfg.cert != null) ''
+                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.cert} ${cfg.configDir}/cert.pem
               ''}
-              ${optionalString (cfg.declarative.key != null) ''
-                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.declarative.key} ${cfg.configDir}/key.pem
+              ${optionalString (cfg.key != null) ''
+                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem
               ''}
             ''}"
           ;
@@ -516,8 +540,10 @@ in {
         };
       };
       syncthing-init = mkIf (
-        cfg.declarative.devices != {} || cfg.declarative.folders != {}
+        cfg.devices != {} || cfg.folders != {} || cfg.extraOptions != {}
       ) {
+        description = "Syncthing configuration updater";
+        requisite = [ "syncthing.service" ];
         after = [ "syncthing.service" ];
         wantedBy = [ "multi-user.target" ];
 
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index c0a4ce40760..494d21cc867 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -241,7 +241,8 @@ in {
     environment.systemPackages = [ package ];
 
     services.dbus.packages = [ package ];
-    services.udev.packages = [ pkgs.crda ];
+
+    hardware.wirelessRegulatoryDatabase = true;
 
     # FIXME: start a separate wpa_supplicant instance per interface.
     systemd.services.wpa_supplicant = let
diff --git a/nixos/modules/services/system/uptimed.nix b/nixos/modules/services/system/uptimed.nix
index 1e256c51408..67a03876e19 100644
--- a/nixos/modules/services/system/uptimed.nix
+++ b/nixos/modules/services/system/uptimed.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.uptimed;
-  stateDir = "/var/spool/uptimed";
+  stateDir = "/var/lib/uptimed";
 in
 {
   options = {
@@ -21,12 +21,16 @@ in
   };
 
   config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.uptimed ];
+
     users.users.uptimed = {
       description = "Uptimed daemon user";
       home        = stateDir;
-      createHome  = true;
       uid         = config.ids.uids.uptimed;
+      group       = "uptimed";
     };
+    users.groups.uptimed = {};
 
     systemd.services.uptimed = {
       unitConfig.Documentation = "man:uptimed(8) man:uprecords(1)";
@@ -41,7 +45,7 @@ in
         PrivateTmp              = "yes";
         PrivateNetwork          = "yes";
         NoNewPrivileges         = "yes";
-        ReadWriteDirectories    = stateDir;
+        StateDirectory          = [ "uptimed" ];
         InaccessibleDirectories = "/home";
         ExecStart               = "${pkgs.uptimed}/sbin/uptimed -f -p ${stateDir}/pid";
       };
diff --git a/nixos/modules/services/web-apps/node-red.nix b/nixos/modules/services/web-apps/node-red.nix
new file mode 100644
index 00000000000..16cfb29cf57
--- /dev/null
+++ b/nixos/modules/services/web-apps/node-red.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.node-red;
+  defaultUser = "node-red";
+  finalPackage = if cfg.withNpmAndGcc then node-red_withNpmAndGcc else cfg.package;
+  node-red_withNpmAndGcc = pkgs.runCommandNoCC "node-red" {
+    nativeBuildInputs = [ pkgs.makeWrapper ];
+  }
+  ''
+    mkdir -p $out/bin
+    makeWrapper ${pkgs.nodePackages.node-red}/bin/node-red $out/bin/node-red \
+      --set PATH '${lib.makeBinPath [ pkgs.nodePackages.npm pkgs.gcc ]}:$PATH' \
+  '';
+in
+{
+  options.services.node-red = {
+    enable = mkEnableOption "the Node-RED service";
+
+    package = mkOption {
+      default = pkgs.nodePackages.node-red;
+      defaultText = "pkgs.nodePackages.node-red";
+      type = types.package;
+      description = "Node-RED package to use.";
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open ports in the firewall for the server.
+      '';
+    };
+
+    withNpmAndGcc = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Give Node-RED access to NPM and GCC at runtime, so 'Nodes' can be
+        downloaded and managed imperatively via the 'Palette Manager'.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      default = "${cfg.package}/lib/node_modules/node-red/settings.js";
+      defaultText = "\${cfg.package}/lib/node_modules/node-red/settings.js";
+      description = ''
+        Path to the JavaScript configuration file.
+        See <link
+        xlink:href="https://github.com/node-red/node-red/blob/master/packages/node_modules/node-red/settings.js"/>
+        for a configuration example.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 1880;
+      description = "Listening port.";
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = ''
+        User under which Node-RED runs.If left as the default value this user
+        will automatically be created on system activation, otherwise the
+        sysadmin is responsible for ensuring the user exists.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = ''
+        Group under which Node-RED runs.If left as the default value this group
+        will automatically be created on system activation, otherwise the
+        sysadmin is responsible for ensuring the group exists.
+      '';
+    };
+
+    userDir = mkOption {
+      type = types.path;
+      default = "/var/lib/node-red";
+      description = ''
+        The directory to store all user data, such as flow and credential files and all library data. If left
+        as the default value this directory will automatically be created before the node-red service starts,
+        otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+        and permissions.
+      '';
+    };
+
+    safe = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to launch Node-RED in --safe mode.";
+    };
+
+    define = mkOption {
+      type = types.attrs;
+      default = {};
+      description = "List of settings.js overrides to pass via -D to Node-RED.";
+      example = literalExample ''
+        {
+          "logging.console.level" = "trace";
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == defaultUser) {
+      ${defaultUser} = { };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    systemd.services.node-red = {
+      description = "Node-RED Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      environment = {
+        HOME = cfg.userDir;
+      };
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${finalPackage}/bin/node-red ${pkgs.lib.optionalString cfg.safe "--safe"} --settings ${cfg.configFile} --port ${toString cfg.port} --userDir ${cfg.userDir} ${concatStringsSep " " (mapAttrsToList (name: value: "-D ${name}=${value}") cfg.define)}";
+          PrivateTmp = true;
+          Restart = "always";
+          WorkingDirectory = cfg.userDir;
+        }
+        (mkIf (cfg.userDir == "/var/lib/node-red") { StateDirectory = "node-red"; })
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index b78487cc928..ed13845915c 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -19,82 +19,84 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
 
-  tt-rss-config = pkgs.writeText "config.php" ''
+  tt-rss-config = let
+    password =
+      if (cfg.database.password != null) then
+        "${(escape ["'" "\\"] cfg.database.password)}"
+      else if (cfg.database.passwordFile != null) then
+        "file_get_contents('${cfg.database.passwordFile}'"
+      else
+        ""
+      ;
+  in pkgs.writeText "config.php" ''
     <?php
+      putenv('TTRSS_PHP_EXECUTABLE=${pkgs.php}/bin/php');
 
-      define('PHP_EXECUTABLE', '${pkgs.php}/bin/php');
+      putenv('TTRSS_LOCK_DIRECTORY=${lockDir}');
+      putenv('TTRSS_CACHE_DIR=${cacheDir}');
+      putenv('TTRSS_ICONS_DIR=${feedIconsDir}');
+      putenv('TTRSS_ICONS_URL=${feedIconsDir}');
+      putenv('TTRSS_SELF_URL_PATH=${cfg.selfUrlPath}');
 
-      define('LOCK_DIRECTORY', '${lockDir}');
-      define('CACHE_DIR', '${cacheDir}');
-      define('ICONS_DIR', '${feedIconsDir}');
-      define('ICONS_URL', '${feedIconsDir}');
-      define('SELF_URL_PATH', '${cfg.selfUrlPath}');
+      putenv('TTRSS_MYSQL_CHARSET=UTF8');
 
-      define('MYSQL_CHARSET', 'UTF8');
+      putenv('TTRSS_DB_TYPE=${cfg.database.type}');
+      putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}');
+      putenv('TTRSS_DB_USER=${cfg.database.user}');
+      putenv('TTRSS_DB_NAME=${cfg.database.name}');
+      putenv('TTRSS_DB_PASS=${password}');
+      putenv('TTRSS_DB_PORT=${toString dbPort}');
 
-      define('DB_TYPE', '${cfg.database.type}');
-      define('DB_HOST', '${optionalString (cfg.database.host != null) cfg.database.host}');
-      define('DB_USER', '${cfg.database.user}');
-      define('DB_NAME', '${cfg.database.name}');
-      define('DB_PASS', ${
-        if (cfg.database.password != null) then
-          "'${(escape ["'" "\\"] cfg.database.password)}'"
-        else if (cfg.database.passwordFile != null) then
-          "file_get_contents('${cfg.database.passwordFile}')"
-        else
-          "''"
-      });
-      define('DB_PORT', '${toString dbPort}');
+      putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}');
+      putenv('TTRSS_AUTH_AUTO_LOGIN=${boolToString cfg.auth.autoLogin}');
 
-      define('AUTH_AUTO_CREATE', ${boolToString cfg.auth.autoCreate});
-      define('AUTH_AUTO_LOGIN', ${boolToString cfg.auth.autoLogin});
+      putenv('TTRSS_FEED_CRYPT_KEY=${escape ["'" "\\"] cfg.feedCryptKey}');
 
-      define('FEED_CRYPT_KEY', '${escape ["'" "\\"] cfg.feedCryptKey}');
 
+      putenv('TTRSS_SINGLE_USER_MODE=${boolToString cfg.singleUserMode}');
 
-      define('SINGLE_USER_MODE', ${boolToString cfg.singleUserMode});
+      putenv('TTRSS_SIMPLE_UPDATE_MODE=${boolToString cfg.simpleUpdateMode}');
 
-      define('SIMPLE_UPDATE_MODE', ${boolToString cfg.simpleUpdateMode});
+      # Never check for updates - the running version of the code should
+      # be controlled entirely by the version of TT-RSS active in the
+      # current Nix profile. If TT-RSS updates itself to a version
+      # requiring a database schema upgrade, and then the SystemD
+      # tt-rss.service is restarted, the old code copied from the Nix
+      # store will overwrite the updated version, causing the code to
+      # detect the need for a schema "upgrade" (since the schema version
+      # in the database is different than in the code), but the update
+      # schema operation in TT-RSS will do nothing because the schema
+      # version in the database is newer than that in the code.
+      putenv('TTRSS_CHECK_FOR_UPDATES=false');
 
-      // Never check for updates - the running version of the code should be
-      // controlled entirely by the version of TT-RSS active in the current Nix
-      // profile. If TT-RSS updates itself to a version requiring a database
-      // schema upgrade, and then the SystemD tt-rss.service is restarted, the
-      // old code copied from the Nix store will overwrite the updated version,
-      // causing the code to detect the need for a schema "upgrade" (since the
-      // schema version in the database is different than in the code), but the
-      // update schema operation in TT-RSS will do nothing because the schema
-      // version in the database is newer than that in the code.
-      define('CHECK_FOR_UPDATES', false);
+      putenv('TTRSS_FORCE_ARTICLE_PURGE=${toString cfg.forceArticlePurge}');
+      putenv('TTRSS_SESSION_COOKIE_LIFETIME=${toString cfg.sessionCookieLifetime}');
+      putenv('TTRSS_ENABLE_GZIP_OUTPUT=${boolToString cfg.enableGZipOutput}');
 
-      define('FORCE_ARTICLE_PURGE', ${toString cfg.forceArticlePurge});
-      define('SESSION_COOKIE_LIFETIME', ${toString cfg.sessionCookieLifetime});
-      define('ENABLE_GZIP_OUTPUT', ${boolToString cfg.enableGZipOutput});
+      putenv('TTRSS_PLUGINS=${builtins.concatStringsSep "," cfg.plugins}');
 
-      define('PLUGINS', '${builtins.concatStringsSep "," cfg.plugins}');
+      putenv('TTRSS_LOG_DESTINATION=${cfg.logDestination}');
+      putenv('TTRSS_CONFIG_VERSION=${toString configVersion}');
 
-      define('LOG_DESTINATION', '${cfg.logDestination}');
-      define('CONFIG_VERSION', ${toString configVersion});
 
+      putenv('TTRSS_PUBSUBHUBBUB_ENABLED=${boolToString cfg.pubSubHubbub.enable}');
+      putenv('TTRSS_PUBSUBHUBBUB_HUB=${cfg.pubSubHubbub.hub}');
 
-      define('PUBSUBHUBBUB_ENABLED', ${boolToString cfg.pubSubHubbub.enable});
-      define('PUBSUBHUBBUB_HUB', '${cfg.pubSubHubbub.hub}');
+      putenv('TTRSS_SPHINX_SERVER=${cfg.sphinx.server}');
+      putenv('TTRSS_SPHINX_INDEX=${builtins.concatStringsSep "," cfg.sphinx.index}');
 
-      define('SPHINX_SERVER', '${cfg.sphinx.server}');
-      define('SPHINX_INDEX', '${builtins.concatStringsSep "," cfg.sphinx.index}');
+      putenv('TTRSS_ENABLE_REGISTRATION=${boolToString cfg.registration.enable}');
+      putenv('TTRSS_REG_NOTIFY_ADDRESS=${cfg.registration.notifyAddress}');
+      putenv('TTRSS_REG_MAX_USERS=${toString cfg.registration.maxUsers}');
 
-      define('ENABLE_REGISTRATION', ${boolToString cfg.registration.enable});
-      define('REG_NOTIFY_ADDRESS', '${cfg.registration.notifyAddress}');
-      define('REG_MAX_USERS', ${toString cfg.registration.maxUsers});
+      putenv('TTRSS_SMTP_SERVER=${cfg.email.server}');
+      putenv('TTRSS_SMTP_LOGIN=${cfg.email.login}');
+      putenv('TTRSS_SMTP_PASSWORD=${escape ["'" "\\"] cfg.email.password}');
+      putenv('TTRSS_SMTP_SECURE=${cfg.email.security}');
 
-      define('SMTP_SERVER', '${cfg.email.server}');
-      define('SMTP_LOGIN', '${cfg.email.login}');
-      define('SMTP_PASSWORD', '${escape ["'" "\\"] cfg.email.password}');
-      define('SMTP_SECURE', '${cfg.email.security}');
-
-      define('SMTP_FROM_NAME', '${escape ["'" "\\"] cfg.email.fromName}');
-      define('SMTP_FROM_ADDRESS', '${escape ["'" "\\"] cfg.email.fromAddress}');
-      define('DIGEST_SUBJECT', '${escape ["'" "\\"] cfg.email.digestSubject}');
+      putenv('TTRSS_SMTP_FROM_NAME=${escape ["'" "\\"] cfg.email.fromName}');
+      putenv('TTRSS_SMTP_FROM_ADDRESS=${escape ["'" "\\"] cfg.email.fromAddress}');
+      putenv('TTRSS_DIGEST_SUBJECT=${escape ["'" "\\"] cfg.email.digestSubject}');
 
       ${cfg.extraConfig}
   '';
@@ -564,9 +566,12 @@ let
       "Z '${cfg.root}' 0755 ${cfg.user} tt_rss - -"
     ];
 
-    systemd.services.tt-rss =
-      {
+    systemd.services = {
+      phpfpm-tt-rss = mkIf (cfg.pool == "${poolName}") {
+        restartTriggers = [ tt-rss-config pkgs.tt-rss ];
+      };
 
+      tt-rss = {
         description = "Tiny Tiny RSS feeds update daemon";
 
         preStart = let
@@ -604,6 +609,9 @@ let
           ''}
           ln -sf "${tt-rss-config}" "${cfg.root}/config.php"
           chmod -R 755 "${cfg.root}"
+          chmod -R ug+rwX "${cfg.root}/${lockDir}"
+          chmod -R ug+rwX "${cfg.root}/${cacheDir}"
+          chmod -R ug+rwX "${cfg.root}/${feedIconsDir}"
         ''
 
         + (optionalString (cfg.database.type == "pgsql") ''
@@ -640,6 +648,7 @@ let
         wantedBy = [ "multi-user.target" ];
         requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
         after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      };
     };
 
     services.mysql = mkIf mysqlLocal {
diff --git a/nixos/modules/services/x11/display-managers/set-session.py b/nixos/modules/services/x11/display-managers/set-session.py
index 0cca80af44e..75940efe32b 100755
--- a/nixos/modules/services/x11/display-managers/set-session.py
+++ b/nixos/modules/services/x11/display-managers/set-session.py
@@ -72,11 +72,14 @@ def main():
                     f"Setting session name: {session}, as we found the existing wayland-session: {session_file}"
                 )
                 user.set_session(session)
+                user.set_session_type("wayland")
             elif is_session_xsession(session_file):
                 logging.debug(
                     f"Setting session name: {session}, as we found the existing xsession: {session_file}"
                 )
                 user.set_x_session(session)
+                user.set_session(session)
+                user.set_session_type("x11")
             else:
                 logging.error(f"Couldn't figure out session type for {session_file}")
                 sys.exit(1)
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 58064e5de86..73616065a08 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -1044,7 +1044,7 @@ in
           done
         '' + concatMapStrings (name: optionalString (hasPrefix "tmpfiles.d/" name) ''
           rm -f $out/${removePrefix "tmpfiles.d/" name}
-        '') config.system.build.etc.targets;
+        '') config.system.build.etc.passthru.targets;
       }) + "/*";
 
       "systemd/system-generators" = { source = hooks "generators" cfg.generators; };
@@ -1053,6 +1053,7 @@ in
 
     services.dbus.enable = true;
 
+    users.users.systemd-coredump.uid = config.ids.uids.systemd-coredump;
     users.users.systemd-network.uid = config.ids.uids.systemd-network;
     users.groups.systemd-network.gid = config.ids.gids.systemd-network;
     users.users.systemd-resolve.uid = config.ids.uids.systemd-resolve;
diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix
index a450f303572..183de729210 100644
--- a/nixos/modules/system/etc/etc.nix
+++ b/nixos/modules/system/etc/etc.nix
@@ -8,21 +8,61 @@ let
 
   etc' = filter (f: f.enable) (attrValues config.environment.etc);
 
-  etc = pkgs.stdenvNoCC.mkDerivation {
-    name = "etc";
-
-    builder = ./make-etc.sh;
-
+  etc = pkgs.runCommand "etc" {
     preferLocalBuild = true;
     allowSubstitutes = false;
 
-    /* !!! Use toXML. */
-    sources = map (x: x.source) etc';
-    targets = map (x: x.target) etc';
-    modes = map (x: x.mode) etc';
-    users  = map (x: x.user) etc';
-    groups  = map (x: x.group) etc';
-  };
+    # This is needed for the systemd module
+    passthru.targets = map (x: x.target) etc';
+  } /* sh */ ''
+    set -euo pipefail
+
+    makeEtcEntry() {
+      src="$1"
+      target="$2"
+      mode="$3"
+      user="$4"
+      group="$5"
+
+      if [[ "$src" = *'*'* ]]; then
+        # If the source name contains '*', perform globbing.
+        mkdir -p "$out/etc/$target"
+        for fn in $src; do
+            ln -s "$fn" "$out/etc/$target/"
+        done
+      else
+
+        mkdir -p "$out/etc/$(dirname "$target")"
+        if ! [ -e "$out/etc/$target" ]; then
+          ln -s "$src" "$out/etc/$target"
+        else
+          echo "duplicate entry $target -> $src"
+          if [ "$(readlink "$out/etc/$target")" != "$src" ]; then
+            echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src"
+            ret=1
+
+            continue
+          fi
+        fi
+
+        if [ "$mode" != symlink ]; then
+          echo "$mode" > "$out/etc/$target.mode"
+          echo "$user" > "$out/etc/$target.uid"
+          echo "$group" > "$out/etc/$target.gid"
+        fi
+      fi
+    }
+
+    mkdir -p "$out/etc"
+    ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [
+      "makeEtcEntry"
+      etcEntry.source
+      etcEntry.target
+      etcEntry.mode
+      etcEntry.user
+      etcEntry.group
+    ]) etc'}
+  '';
 
 in
 
diff --git a/nixos/modules/system/etc/make-etc.sh b/nixos/modules/system/etc/make-etc.sh
deleted file mode 100644
index aabfb5e88a6..00000000000
--- a/nixos/modules/system/etc/make-etc.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-source $stdenv/setup
-
-mkdir -p $out/etc
-
-set -f
-sources_=($sources)
-targets_=($targets)
-modes_=($modes)
-users_=($users)
-groups_=($groups)
-set +f
-
-for ((i = 0; i < ${#targets_[@]}; i++)); do
-    source="${sources_[$i]}"
-    target="${targets_[$i]}"
-
-    if [[ "$source" =~ '*' ]]; then
-
-        # If the source name contains '*', perform globbing.
-        mkdir -p $out/etc/$target
-        for fn in $source; do
-            ln -s "$fn" $out/etc/$target/
-        done
-
-    else
-
-        mkdir -p $out/etc/$(dirname $target)
-        if ! [ -e $out/etc/$target ]; then
-            ln -s $source $out/etc/$target
-        else
-            echo "duplicate entry $target -> $source"
-            if test "$(readlink $out/etc/$target)" != "$source"; then
-                echo "mismatched duplicate entry $(readlink $out/etc/$target) <-> $source"
-                exit 1
-            fi
-        fi
-
-        if test "${modes_[$i]}" != symlink; then
-            echo "${modes_[$i]}"  > $out/etc/$target.mode
-            echo "${users_[$i]}"  > $out/etc/$target.uid
-            echo "${groups_[$i]}" > $out/etc/$target.gid
-        fi
-
-    fi
-done
diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix
index 272c696807a..1a0c4df42cb 100644
--- a/nixos/modules/virtualisation/virtualbox-image.nix
+++ b/nixos/modules/virtualisation/virtualbox-image.nix
@@ -18,6 +18,13 @@ in {
           The size of the VirtualBox base image in MiB.
         '';
       };
+      baseImageFreeSpace = mkOption {
+        type = with types; int;
+        default = 30 * 1024;
+        description = ''
+          Free space in the VirtualBox base image in MiB.
+        '';
+      };
       memorySize = mkOption {
         type = types.int;
         default = 1536;
@@ -129,6 +136,7 @@ in {
       inherit pkgs lib config;
       partitionTableType = "legacy";
       diskSize = cfg.baseImageSize;
+      additionalSpace = "${toString cfg.baseImageFreeSpace}M";
 
       postVM =
         ''
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index d6ef7d42431..ce91d651888 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -204,6 +204,7 @@ in
   k3s = handleTest ./k3s.nix {};
   kafka = handleTest ./kafka.nix {};
   kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};
+  kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {};
   kea = handleTest ./kea.nix {};
   keepalived = handleTest ./keepalived.nix {};
   keepassxc = handleTest ./keepassxc.nix {};
@@ -222,10 +223,12 @@ in
   latestKernel.hardened = handleTest ./hardened.nix { latestKernel = true; };
   latestKernel.login = handleTest ./login.nix { latestKernel = true; };
   leaps = handleTest ./leaps.nix {};
+  libreddit = handleTest ./libreddit.nix {};
   lidarr = handleTest ./lidarr.nix {};
   libreswan = handleTest ./libreswan.nix {};
   lightdm = handleTest ./lightdm.nix {};
   limesurvey = handleTest ./limesurvey.nix {};
+  litestream = handleTest ./litestream.nix {};
   locate = handleTest ./locate.nix {};
   login = handleTest ./login.nix {};
   loki = handleTest ./loki.nix {};
@@ -258,6 +261,7 @@ in
   morty = handleTest ./morty.nix {};
   mosquitto = handleTest ./mosquitto.nix {};
   mpd = handleTest ./mpd.nix {};
+  mpv = handleTest ./mpv.nix {};
   mumble = handleTest ./mumble.nix {};
   musescore = handleTest ./musescore.nix {};
   munin = handleTest ./munin.nix {};
@@ -300,6 +304,7 @@ in
   nix-serve = handleTest ./nix-ssh-serve.nix {};
   nix-ssh-serve = handleTest ./nix-ssh-serve.nix {};
   nixos-generate-config = handleTest ./nixos-generate-config.nix {};
+  node-red = handleTest ./node-red.nix {};
   nomad = handleTest ./nomad.nix {};
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
   nsd = handleTest ./nsd.nix {};
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
index 696ebabb580..fc153bb06ec 100644
--- a/nixos/tests/gitlab.nix
+++ b/nixos/tests/gitlab.nix
@@ -51,10 +51,11 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
             host = "localhost";
             port = 143;
           };
-          pages = {
-            enabled = true;
-            host = "localhost";
-          };
+          # https://github.com/NixOS/nixpkgs/issues/132295
+          # pages = {
+          #   enabled = true;
+          #   host = "localhost";
+          # };
         };
         secrets = {
           secretFile = pkgs.writeText "secret" "Aig5zaic";
@@ -90,7 +91,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
       waitForServices = ''
         gitlab.wait_for_unit("gitaly.service")
         gitlab.wait_for_unit("gitlab-workhorse.service")
-        gitlab.wait_for_unit("gitlab-pages.service")
+        # https://github.com/NixOS/nixpkgs/issues/132295
+        # gitlab.wait_for_unit("gitlab-pages.service")
         gitlab.wait_for_unit("gitlab-mailroom.service")
         gitlab.wait_for_unit("gitlab.service")
         gitlab.wait_for_unit("gitlab-sidekiq.service")
diff --git a/nixos/tests/kbd-update-search-paths-patch.nix b/nixos/tests/kbd-update-search-paths-patch.nix
new file mode 100644
index 00000000000..2967ee48903
--- /dev/null
+++ b/nixos/tests/kbd-update-search-paths-patch.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "kbd-update-search-paths-patch";
+
+  machine = { pkgs, options, ... }: {
+    console = {
+      packages = options.console.packages.default ++ [ pkgs.terminus_font ];
+    };
+  };
+
+  testScript = ''
+    command = "${pkgs.kbd}/bin/setfont ter-112n 2>&1"
+    (status, out) = machine.execute(command)
+    pattern = re.compile(r".*Unable to find file:.*")
+    match = pattern.match(out)
+    if match:
+        raise Exception("command `{}` failed".format(command))
+  '';
+})
diff --git a/nixos/tests/libreddit.nix b/nixos/tests/libreddit.nix
new file mode 100644
index 00000000000..f7ef701d086
--- /dev/null
+++ b/nixos/tests/libreddit.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "libreddit";
+  meta.maintainers = with maintainers; [ fab ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.libreddit.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("libreddit.service")
+    machine.wait_for_open_port("8080")
+    # The service wants to get data from https://www.reddit.com
+    machine.succeed("curl http://localhost:8080/")
+  '';
+})
diff --git a/nixos/tests/litestream.nix b/nixos/tests/litestream.nix
new file mode 100644
index 00000000000..886fbfef9cf
--- /dev/null
+++ b/nixos/tests/litestream.nix
@@ -0,0 +1,93 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "litestream";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ jwygoda ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    { services.litestream = {
+        enable = true;
+        settings = {
+          dbs = [
+            {
+              path = "/var/lib/grafana/data/grafana.db";
+              replicas = [{
+                url = "sftp://foo:bar@127.0.0.1:22/home/foo/grafana";
+              }];
+            }
+          ];
+        };
+      };
+      systemd.services.grafana.serviceConfig.ExecStartPost = "+" + pkgs.writeShellScript "grant-grafana-permissions" ''
+        timeout=10
+
+        while [ ! -f /var/lib/grafana/data/grafana.db ];
+        do
+          if [ "$timeout" == 0 ]; then
+            echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
+            exit 1
+          fi
+
+          sleep 1
+
+          ((timeout--))
+        done
+
+        find /var/lib/grafana -type d -exec chmod -v 775 {} \;
+        find /var/lib/grafana -type f -exec chmod -v 660 {} \;
+      '';
+      services.openssh = {
+        enable = true;
+        allowSFTP = true;
+        listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+      };
+      services.grafana = {
+        enable = true;
+        security = {
+          adminUser = "admin";
+          adminPassword = "admin";
+        };
+        addr = "localhost";
+        port = 3000;
+        extraOptions = {
+          DATABASE_URL = "sqlite3:///var/lib/grafana/data/grafana.db?cache=private&mode=rwc&_journal_mode=WAL";
+        };
+      };
+      users.users.foo = {
+        isNormalUser = true;
+        password = "bar";
+      };
+      users.users.litestream.extraGroups = [ "grafana" ];
+    };
+
+  testScript = ''
+    start_all()
+    machine.wait_until_succeeds("test -d /home/foo/grafana")
+    machine.wait_for_open_port(3000)
+    machine.succeed("""
+        curl -sSfN -X PUT -H "Content-Type: application/json" -d '{
+          "oldPassword": "admin",
+          "newPassword": "newpass",
+          "confirmNew": "newpass"
+        }' http://admin:admin@127.0.0.1:3000/api/user/password
+    """)
+    # https://litestream.io/guides/systemd/#simulating-a-disaster
+    machine.systemctl("stop litestream.service")
+    machine.succeed(
+        "rm -f /var/lib/grafana/data/grafana.db "
+        "/var/lib/grafana/data/grafana.db-shm "
+        "/var/lib/grafana/data/grafana.db-wal"
+    )
+    machine.succeed(
+        "litestream restore /var/lib/grafana/data/grafana.db "
+        "&& chown grafana:grafana /var/lib/grafana/data/grafana.db "
+        "&& chmod 660 /var/lib/grafana/data/grafana.db"
+    )
+    machine.systemctl("restart grafana.service")
+    machine.wait_for_open_port(3000)
+    machine.succeed(
+        "curl -sSfN -u admin:newpass http://127.0.0.1:3000/api/org/users | grep admin\@localhost"
+    )
+  '';
+})
diff --git a/nixos/tests/mpv.nix b/nixos/tests/mpv.nix
new file mode 100644
index 00000000000..bcfc17cf332
--- /dev/null
+++ b/nixos/tests/mpv.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+let
+  port = toString 4321;
+in
+{
+  name = "mpv";
+  meta.maintainers = with maintainers; [ zopieux ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      environment.systemPackages = [
+        pkgs.curl
+        (pkgs.mpv-with-scripts.override {
+          scripts = [ pkgs.mpvScripts.simple-mpv-webui ];
+        })
+      ];
+    };
+
+  testScript = ''
+    machine.execute("set -m; mpv --script-opts=webui-port=${port} --idle=yes &")
+    machine.wait_for_open_port(${port})
+    assert "<title>simple-mpv-webui" in machine.succeed("curl -s localhost:${port}")
+  '';
+})
diff --git a/nixos/tests/node-red.nix b/nixos/tests/node-red.nix
new file mode 100644
index 00000000000..7660bc32f4c
--- /dev/null
+++ b/nixos/tests/node-red.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nodered";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ matthewcroughan ];
+  };
+
+  nodes = {
+    client = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.curl ];
+    };
+    nodered = { config, pkgs, ... }: {
+      services.node-red = {
+        enable = true;
+        openFirewall = true;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    nodered.wait_for_unit("node-red.service")
+    nodered.wait_for_open_port("1880")
+
+    client.wait_for_unit("multi-user.target")
+
+    with subtest("Check that the Node-RED webserver can be reached."):
+        assert "<title>Node-RED</title>" in client.succeed(
+            "curl -sSf http:/nodered:1880/ | grep title"
+        )
+  '';
+})
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index e8bc6339ecf..6153ba9e366 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -273,6 +273,26 @@ let
       '';
     };
 
+    influxdb = {
+      exporterConfig = {
+        enable = true;
+        sampleExpiry = "3s";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-influxdb-exporter.service")
+        succeed(
+          "curl -XPOST http://localhost:9122/write --data-binary 'influxdb_exporter,distro=nixos,added_in=21.09 value=1'"
+        )
+        succeed(
+          "curl -sSf http://localhost:9122/metrics | grep 'nixos'"
+        )
+        execute("sleep 5")
+        fail(
+          "curl -sSf http://localhost:9122/metrics | grep 'nixos'"
+        )
+      '';
+    };
+
     jitsi = {
       exporterConfig = {
         enable = true;
diff --git a/nixos/tests/rabbitmq.nix b/nixos/tests/rabbitmq.nix
index 8a7fcc0e899..69be29b0f9e 100644
--- a/nixos/tests/rabbitmq.nix
+++ b/nixos/tests/rabbitmq.nix
@@ -8,6 +8,8 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   machine = {
     services.rabbitmq.enable = true;
+    # Ensure there is sufficient extra disk space for rabbitmq to be happy
+    virtualisation.diskSize = 1024;
   };
 
   testScript = ''
diff --git a/nixos/tests/soapui.nix b/nixos/tests/soapui.nix
new file mode 100644
index 00000000000..205128df91f
--- /dev/null
+++ b/nixos/tests/soapui.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "soapui";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ asbachb ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+
+    environment.systemPackages = [ pkgs.soapui ];
+  };
+
+  testScript = ''
+    machine.wait_for_x()
+    machine.succeed("soapui &")
+    machine.wait_for_window(r"SoapUI \d+\.\d+\.\d+")
+    machine.sleep(1)
+    machine.screenshot("soapui")
+  '';
+})
diff --git a/nixos/tests/syncthing-init.nix b/nixos/tests/syncthing-init.nix
index 4581e3fd4fb..8b60ad7faf0 100644
--- a/nixos/tests/syncthing-init.nix
+++ b/nixos/tests/syncthing-init.nix
@@ -9,15 +9,14 @@ in {
   machine = {
     services.syncthing = {
       enable = true;
-      declarative = {
-        devices.testDevice = {
-          id = testId;
-        };
-        folders.testFolder = {
-          path = "/tmp/test";
-          devices = [ "testDevice" ];
-        };
+      devices.testDevice = {
+        id = testId;
       };
+      folders.testFolder = {
+        path = "/tmp/test";
+        devices = [ "testDevice" ];
+      };
+      extraOptions.gui.user = "guiUser";
     };
   };
 
@@ -27,5 +26,6 @@ in {
 
     assert "testFolder" in config
     assert "${testId}" in config
+    assert "guiUser" in config
   '';
 })
diff --git a/nixos/tests/syncthing.nix b/nixos/tests/syncthing.nix
index 5536b7055cc..aff1d874413 100644
--- a/nixos/tests/syncthing.nix
+++ b/nixos/tests/syncthing.nix
@@ -25,7 +25,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
             "xmllint --xpath 'string(configuration/gui/apikey)' %s/config.xml" % confdir
         ).strip()
         oldConf = host.succeed(
-            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/system/config" % APIKey
+            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/config" % APIKey
         )
         conf = json.loads(oldConf)
         conf["devices"].append({"deviceID": deviceID, "id": name})
@@ -39,7 +39,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
         )
         newConf = json.dumps(conf)
         host.succeed(
-            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/system/config -d %s"
+            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/config -X PUT -d %s"
             % (APIKey, shlex.quote(newConf))
         )