summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-1709.xml18
-rw-r--r--nixos/modules/config/pulseaudio.nix2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/profiles/all-hardware.nix3
-rw-r--r--nixos/modules/programs/gnupg.nix2
-rw-r--r--nixos/modules/services/audio/alsa.nix4
-rw-r--r--nixos/modules/services/misc/nixos-manual.nix5
-rw-r--r--nixos/modules/services/misc/snapper.nix152
-rw-r--r--nixos/modules/services/misc/taskserver/helper-tool.py2
-rw-r--r--nixos/modules/services/networking/bitlbee.nix27
-rw-r--r--nixos/modules/services/networking/strongswan.nix2
-rw-r--r--nixos/modules/services/networking/tinc.nix13
-rw-r--r--nixos/modules/services/networking/wireguard.nix85
-rw-r--r--nixos/modules/services/printing/cupsd.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix53
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix26
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh1
-rw-r--r--nixos/release.nix1
-rw-r--r--nixos/tests/snapper.nix43
-rw-r--r--nixos/tests/taskserver.nix4
20 files changed, 379 insertions, 67 deletions
diff --git a/nixos/doc/manual/release-notes/rl-1709.xml b/nixos/doc/manual/release-notes/rl-1709.xml
index 34cfe1702e9c..72dfd60bedd9 100644
--- a/nixos/doc/manual/release-notes/rl-1709.xml
+++ b/nixos/doc/manual/release-notes/rl-1709.xml
@@ -86,6 +86,10 @@ rmdir /var/lib/ipfs/.ipfs
   </listitem>
   <listitem>
     <para>
+      The following changes apply if the <literal>stateVersion</literal> is changed to 17.09 or higher.
+      For <literal>stateVersion = "17.03</literal> or lower the old behavior is preserved.
+    </para>
+    <para>
       The <literal>postgres</literal> default version was changed from 9.5 to 9.6.
     </para>
     <para>
@@ -94,6 +98,9 @@ rmdir /var/lib/ipfs/.ipfs
     <para>
       The <literal>postgres</literal> default <literal>dataDir</literal> has changed from <literal>/var/db/postgres</literal> to <literal>/var/lib/postgresql/$psqlSchema</literal> where $psqlSchema is 9.6 for example.
     </para>
+    <para>
+      The <literal>mysql</literal> default <literal>dataDir</literal> has changed from <literal>/var/mysql</literal> to <literal>/var/lib/mysql</literal>.
+    </para>
   </listitem>
   <listitem>
     <para>
@@ -113,9 +120,18 @@ rmdir /var/lib/ipfs/.ipfs
       also serve as a SSH agent if <literal>enableSSHSupport</literal> is set.
     </para>
   </listitem>
+  <listitem>
+    <para>
+      The <literal>services.tinc.networks.&lt;name&gt;.listenAddress</literal>
+      option had a misleading name that did not correspond to its behavior. It
+      now correctly defines the ip to listen for incoming connections on. To
+      keep the previous behaviour, use
+      <literal>services.tinc.networks.&lt;name&gt;.bindToAddress</literal>
+      instead. Refer to the description of the options for more details.
+    </para>
+  </listitem>
 </itemizedlist>
 
-
 <para>Other notable improvements:</para>
 
 <itemizedlist>
diff --git a/nixos/modules/config/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
index bd80c8113483..b12ef2fe861d 100644
--- a/nixos/modules/config/pulseaudio.nix
+++ b/nixos/modules/config/pulseaudio.nix
@@ -6,6 +6,7 @@ with lib;
 let
 
   cfg = config.hardware.pulseaudio;
+  alsaCfg = config.sound;
 
   systemWide = cfg.enable && cfg.systemWide;
   nonSystemWide = cfg.enable && !cfg.systemWide;
@@ -76,6 +77,7 @@ let
     ctl.!default {
       type pulse
     }
+    ${alsaCfg.extraConfig}
   '');
 
 in {
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 7194e1f8385a..726c55539190 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -327,6 +327,7 @@
   ./services/misc/ripple-data-api.nix
   ./services/misc/rogue.nix
   ./services/misc/siproxd.nix
+  ./services/misc/snapper.nix
   ./services/misc/sonarr.nix
   ./services/misc/spice-vdagentd.nix
   ./services/misc/ssm-agent.nix
diff --git a/nixos/modules/profiles/all-hardware.nix b/nixos/modules/profiles/all-hardware.nix
index 530b2fbffd1c..6e6ae98e19fc 100644
--- a/nixos/modules/profiles/all-hardware.nix
+++ b/nixos/modules/profiles/all-hardware.nix
@@ -41,6 +41,9 @@
 
       # Virtio (QEMU, KVM etc.) support.
       "virtio_net" "virtio_pci" "virtio_blk" "virtio_scsi" "virtio_balloon" "virtio_console"
+      
+      # VMware support.
+      "mptspi" "vmw_balloon" "vmwgfx" "vmw_vmci" "vmw_vsock_vmci_transport" "vmxnet3" "vsock"
 
       # Hyper-V support.
       "hv_storvsc"
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index ea46d5934d9f..8af55f38992f 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -77,7 +77,7 @@ in
 
     systemd.packages = [ pkgs.gnupg ];
 
-    environment.interactiveShellInit = ''
+    environment.extraInit = ''
       # Bind gpg-agent to this TTY if gpg commands are used.
       export GPG_TTY=$(tty)
 
diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix
index 53786dbc6270..acf48d3c3d03 100644
--- a/nixos/modules/services/audio/alsa.nix
+++ b/nixos/modules/services/audio/alsa.nix
@@ -7,6 +7,8 @@ let
 
   inherit (pkgs) alsaUtils;
 
+  pulseaudioEnabled = config.hardware.pulseaudio.enable;
+
 in
 
 {
@@ -80,7 +82,7 @@ in
 
     environment.systemPackages = [ alsaUtils ];
 
-    environment.etc = mkIf (config.sound.extraConfig != "")
+    environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "")
       [
         { source = pkgs.writeText "asound.conf" config.sound.extraConfig;
           target = "asound.conf";
diff --git a/nixos/modules/services/misc/nixos-manual.nix b/nixos/modules/services/misc/nixos-manual.nix
index 622607f3b32d..515864ec2e2d 100644
--- a/nixos/modules/services/misc/nixos-manual.nix
+++ b/nixos/modules/services/misc/nixos-manual.nix
@@ -62,8 +62,7 @@ let
     name = "nixos-manual";
     desktopName = "NixOS Manual";
     genericName = "View NixOS documentation in a web browser";
-    # TODO: find a better icon (Nix logo + help overlay?)
-    icon = "system-help";
+    icon = "nix-snowflake";
     exec = "${helpScript}/bin/nixos-help";
     categories = "System";
   };
@@ -115,7 +114,7 @@ in
 
     environment.systemPackages =
       [ manual.manual helpScript ]
-      ++ optional config.services.xserver.enable desktopItem
+      ++ optionals config.services.xserver.enable [desktopItem pkgs.nixos-icons]
       ++ optional config.programs.man.enable manual.manpages;
 
     boot.extraTTYs = mkIf cfg.showManual ["tty${toString cfg.ttyNumber}"];
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
new file mode 100644
index 000000000000..62b344d11b06
--- /dev/null
+++ b/nixos/modules/services/misc/snapper.nix
@@ -0,0 +1,152 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snapper;
+in
+
+{
+  options.services.snapper = {
+
+    snapshotInterval = mkOption {
+      type = types.str;
+      default = "hourly";
+      description = ''
+        Snapshot interval.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    cleanupInterval = mkOption {
+      type = types.str;
+      default = "1d";
+      description = ''
+        Cleanup interval.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    filters = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Global display difference filter. See man:snapper(8) for more details.
+      '';
+    };
+
+    configs = mkOption {
+      default = { };
+      example = literalExample {
+        "home" = {
+          subvolume = "/home";
+          extraConfig = ''
+            ALLOW_USERS="alice"
+          '';
+        };
+      };
+
+      description = ''
+        Subvolume configuration
+      '';
+
+      type = types.attrsOf (types.submodule {
+        options = {
+          subvolume = mkOption {
+            type = types.path;
+            description = ''
+              Path of the subvolume or mount point.
+              This path is a subvolume and has to contain a subvolume named
+              .snapshots.
+              See also man:snapper(8) section PERMISSIONS.
+            '';
+          };
+
+          fstype = mkOption {
+            type = types.enum [ "btrfs" ];
+            default = "btrfs";
+            description = ''
+              Filesystem type. Only btrfs is stable and tested.
+            '';
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Additional configuration next to SUBVOLUME and FSTYPE.
+              See man:snapper-configs(5).
+            '';
+          };
+        };
+      });
+    };
+  };
+
+  config = mkIf (cfg.configs != {}) (let
+    documentation = [ "man:snapper(8)" "man:snapper-configs(5)" ];
+  in {
+
+    environment = {
+
+      systemPackages = [ pkgs.snapper ];
+
+      # Note: snapper/config-templates/default is only needed for create-config
+      #       which is not the NixOS way to configure.
+      etc = {
+
+        "sysconfig/snapper".text = ''
+          SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}"
+        '';
+
+      }
+      // (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({
+        text = ''
+          ${subvolume.extraConfig}
+          FSTYPE="${subvolume.fstype}"
+          SUBVOLUME="${subvolume.subvolume}"
+        '';
+      })) cfg.configs)
+      // (lib.optionalAttrs (cfg.filters != null) {
+        "snapper/filters/default.txt".text = cfg.filters;
+      });
+
+    };
+
+    services.dbus.packages = [ pkgs.snapper ];
+
+    systemd.services.snapper-timeline = {
+      description = "Timeline of Snapper Snapshots";
+      inherit documentation;
+      serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline";
+    };
+
+    systemd.timers.snapper-timeline = {
+      description = "Timeline of Snapper Snapshots";
+      inherit documentation;
+      wantedBy = [ "basic.target" ];
+      timerConfig.OnCalendar = cfg.snapshotInterval;
+    };
+
+    systemd.services.snapper-cleanup = {
+      description = "Cleanup of Snapper Snapshots";
+      inherit documentation;
+      serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup";
+    };
+
+    systemd.timers.snapper-cleanup = {
+      description = "Cleanup of Snapper Snapshots";
+      inherit documentation;
+      wantedBy = [ "basic.target" ];
+      timerConfig.OnBootSec = "10m";
+      timerConfig.OnUnitActiveSec = cfg.cleanupInterval;
+    };
+  });
+}
+
diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py
index b97bc1df74f7..22a3d8d5311b 100644
--- a/nixos/modules/services/misc/taskserver/helper-tool.py
+++ b/nixos/modules/services/misc/taskserver/helper-tool.py
@@ -448,6 +448,8 @@ def cli(ctx):
     """
     Manage Taskserver users and certificates
     """
+    if not IS_AUTO_CONFIG:
+        return
     for path in (CA_KEY, CA_CERT, CRL_FILE):
         if not os.path.exists(path):
             msg = "CA setup not done or incomplete, missing file {}."
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index e72ea20cccee..bd26804788f3 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -7,6 +7,10 @@ let
   cfg = config.services.bitlbee;
   bitlbeeUid = config.ids.uids.bitlbee;
 
+  bitlbeePkg = if cfg.libpurple_plugins == []
+  then pkgs.bitlbee
+  else pkgs.bitlbee.override { enableLibPurple = true; };
+
   bitlbeeConfig = pkgs.writeText "bitlbee.conf"
     ''
     [settings]
@@ -25,6 +29,12 @@ let
     ${cfg.extraDefaults}
     '';
 
+  purple_plugin_path =
+    lib.concatMapStringsSep ":"
+      (plugin: "${plugin}/lib/pidgin/")
+      cfg.libpurple_plugins
+    ;
+
 in
 
 {
@@ -90,6 +100,15 @@ in
         '';
       };
 
+      libpurple_plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExample "[ pkgs.purple-matrix ]";
+        description = ''
+          The list of libpurple plugins to install.
+        '';
+      };
+
       configDir = mkOption {
         default = "/var/lib/bitlbee";
         type = types.path;
@@ -144,14 +163,16 @@ in
       };
 
     systemd.services.bitlbee =
-      { description = "BitlBee IRC to other chat networks gateway";
+      {
+        environment.PURPLE_PLUGIN_PATH = purple_plugin_path;
+        description = "BitlBee IRC to other chat networks gateway";
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig.User = "bitlbee";
-        serviceConfig.ExecStart = "${pkgs.bitlbee}/sbin/bitlbee -F -n -c ${bitlbeeConfig}";
+        serviceConfig.ExecStart = "${bitlbeePkg}/sbin/bitlbee -F -n -c ${bitlbeeConfig}";
       };
 
-    environment.systemPackages = [ pkgs.bitlbee ];
+    environment.systemPackages = [ bitlbeePkg ];
 
   };
 
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 8778b0364f9a..b0eb0460b9ba 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -120,7 +120,7 @@ in
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [ kmod iproute iptables utillinux ]; # XXX Linux
       wants = [ "keys.target" ];
-      after = [ "network.target" "keys.target" ];
+      after = [ "network-online.target" "keys.target" ];
       environment = {
         STRONGSWAN_CONF = strongswanConf { inherit setup connections ca secrets; };
       };
diff --git a/nixos/modules/services/networking/tinc.nix b/nixos/modules/services/networking/tinc.nix
index 79a0aa953feb..7376d2d24a0b 100644
--- a/nixos/modules/services/networking/tinc.nix
+++ b/nixos/modules/services/networking/tinc.nix
@@ -79,7 +79,15 @@ in
               default = null;
               type = types.nullOr types.str;
               description = ''
-                The ip adress to bind to.
+                The ip address to listen on for incoming connections.
+              '';
+            };
+
+            bindToAddress = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The ip address to bind to (both listen on and send packets from).
               '';
             };
 
@@ -131,7 +139,8 @@ in
               Name = ${if data.name == null then "$HOST" else data.name}
               DeviceType = ${data.interfaceType}
               ${optionalString (data.ed25519PrivateKeyFile != null) "Ed25519PrivateKeyFile = ${data.ed25519PrivateKeyFile}"}
-              ${optionalString (data.listenAddress != null) "BindToAddress = ${data.listenAddress}"}
+              ${optionalString (data.listenAddress != null) "ListenAddress = ${data.listenAddress}"}
+              ${optionalString (data.bindToAddress != null) "BindToAddress = ${data.bindToAddress}"}
               Device = /dev/net/tun
               Interface = tinc.${network}
               ${data.extraConfig}
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 62ff708d244c..d5b21ef1a23b 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -23,8 +23,23 @@ let
 
       privateKey = mkOption {
         example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
-        type = types.str;
-        description = "Base64 private key generated by wg genkey.";
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Base64 private key generated by wg genkey.
+
+          Warning: Consider using privateKeyFile instead if you do not
+          want to store the key in the world-readable Nix store.
+        '';
+      };
+
+      privateKeyFile = mkOption {
+        example = "/private/wireguard_key";
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Private key file as generated by wg genkey.
+        '';
       };
 
       listenPort = mkOption {
@@ -91,7 +106,22 @@ let
         example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
         type = with types; nullOr str;
         description = ''
-          base64 preshared key generated by wg genpsk. Optional,
+          Base64 preshared key generated by wg genpsk. Optional,
+          and may be omitted. This option adds an additional layer of
+          symmetric-key cryptography to be mixed into the already existing
+          public-key cryptography, for post-quantum resistance.
+
+          Warning: Consider using presharedKeyFile instead if you do not
+          want to store the key in the world-readable Nix store.
+        '';
+      };
+
+      presharedKeyFile = mkOption {
+        default = null;
+        example = "/private/wireguard_psk";
+        type = with types; nullOr str;
+        description = ''
+          File pointing to preshared key as generated by wg pensk. Optional,
           and may be omitted. This option adds an additional layer of
           symmetric-key cryptography to be mixed into the already existing
           public-key cryptography, for post-quantum resistance.
@@ -134,54 +164,59 @@ let
 
   };
 
-  generateConf = name: values: pkgs.writeText "wireguard-${name}.conf" ''
-    [Interface]
-    PrivateKey = ${values.privateKey}
-    ${optionalString (values.listenPort != null)   "ListenPort = ${toString values.listenPort}"}
-
-    ${concatStringsSep "\n\n" (map (peer: ''
-    [Peer]
-    PublicKey = ${peer.publicKey}
-    ${optionalString (peer.presharedKey != null) "PresharedKey = ${peer.presharedKey}"}
-    ${optionalString (peer.allowedIPs != []) "AllowedIPs = ${concatStringsSep ", " peer.allowedIPs}"}
-    ${optionalString (peer.endpoint != null) "Endpoint = ${peer.endpoint}"}
-    ${optionalString (peer.persistentKeepalive != null) "PersistentKeepalive = ${toString peer.persistentKeepalive}"}
-    '') values.peers)}
-  '';
-
   ipCommand = "${pkgs.iproute}/bin/ip";
   wgCommand = "${pkgs.wireguard}/bin/wg";
 
   generateUnit = name: values:
+    # exactly one way to specify the private key must be set
+    assert (values.privateKey != null) != (values.privateKeyFile != null);
+    let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey;
+    in
     nameValuePair "wireguard-${name}"
       {
         description = "WireGuard Tunnel - ${name}";
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
+
         serviceConfig = {
           Type = "oneshot";
           RemainAfterExit = true;
-          ExecStart = lib.flatten([
+          ExecStart = flatten([
             values.preSetup
 
             "-${ipCommand} link del dev ${name}"
             "${ipCommand} link add dev ${name} type wireguard"
-            "${wgCommand} setconf ${name} ${generateConf name values}"
 
             (map (ip:
-            ''${ipCommand} address add ${ip} dev ${name}''
+            "${ipCommand} address add ${ip} dev ${name}"
             ) values.ips)
 
+            ("${wgCommand} set ${name} private-key ${privKey}" +
+            optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}")
+
+            (map (peer:
+            assert (peer.presharedKeyFile == null) || (peer.presharedKey == null); # at most one of the two must be set
+            let psk = if peer.presharedKey != null then pkgs.writeText "wg-psk" peer.presharedKey else peer.presharedKeyFile;
+            in
+            "${wgCommand} set ${name} peer ${peer.publicKey}" +
+            optionalString (psk != null) " preshared-key ${psk}" +
+            optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
+            optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
+            optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}"
+            ) values.peers)
+
             "${ipCommand} link set up dev ${name}"
 
-            (flatten (map (peer: (map (ip:
+            (map (peer: (map (ip:
             "${ipCommand} route add ${ip} dev ${name}"
-            ) peer.allowedIPs)) values.peers))
+            ) peer.allowedIPs)) values.peers)
 
             values.postSetup
           ]);
-
-          ExecStop = [ ''${ipCommand} link del dev "${name}"'' ] ++ values.postShutdown;
+          ExecStop = flatten([
+            "${ipCommand} link del dev ${name}"
+            values.postShutdown
+          ]);
         };
       };
 
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index 7ce2ae38fb36..ba9f99e6a8fb 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -324,6 +324,8 @@ in
               fi
             ''}
           '';
+
+          serviceConfig.PrivateTmp = true;
       };
 
     systemd.services.cups-browsed = mkIf avahiEnabled
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index ae14aa28ae34..2310912d0fde 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -65,6 +65,7 @@ let
         gzip_proxied any;
         gzip_comp_level 9;
         gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+        gzip_vary on;
       ''}
 
       ${optionalString (cfg.recommendedProxySettings) ''
@@ -123,45 +124,49 @@ let
 
   vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost:
       let
-        serverName = vhost.serverName;
         ssl = vhost.enableSSL || vhost.forceSSL;
-        port = if vhost.port != null then vhost.port else (if ssl then 443 else 80);
-        listenString = toString port + optionalString ssl " ssl http2"
-          + optionalString vhost.default " default_server";
-        acmeLocation = optionalString vhost.enableACME (''
+        defaultPort = if ssl then 443 else 80;
+
+        listenString = { addr, port, ... }:
+          "listen ${addr}:${toString (if port != null then port else defaultPort)} "
+          + optionalString ssl "ssl http2 "
+          + optionalString vhost.default "default_server"
+          + ";";
+
+        redirectListenString = { addr, ... }:
+          "listen ${addr}:80 ${optionalString vhost.default "default_server"};";
+
+        acmeLocation = ''
           location /.well-known/acme-challenge {
             ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
             root ${vhost.acmeRoot};
             auth_basic off;
           }
-        '' + (optionalString (vhost.acmeFallbackHost != null) ''
-          location @acme-fallback {
-            auth_basic off;
-            proxy_pass http://${vhost.acmeFallbackHost};
-          }
-        ''));
+          ${optionalString (vhost.acmeFallbackHost != null) ''
+            location @acme-fallback {
+              auth_basic off;
+              proxy_pass http://${vhost.acmeFallbackHost};
+            }
+          ''}
+        '';
+
       in ''
         ${optionalString vhost.forceSSL ''
           server {
-            listen 80 ${optionalString vhost.default "default_server"};
-            ${optionalString enableIPv6
-              ''listen [::]:80 ${optionalString vhost.default "default_server"};''
-            }
+            ${concatMapStringsSep "\n" redirectListenString vhost.listen}
 
-            server_name ${serverName} ${concatStringsSep " " vhost.serverAliases};
-            ${acmeLocation}
+            server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
+            ${optionalString vhost.enableACME acmeLocation}
             location / {
-              return 301 https://$host${optionalString (port != 443) ":${toString port}"}$request_uri;
+              return 301 https://$host$request_uri;
             }
           }
         ''}
 
         server {
-          listen ${listenString};
-          ${optionalString enableIPv6 "listen [::]:${listenString};"}
-
-          server_name ${serverName} ${concatStringsSep " " vhost.serverAliases};
-          ${acmeLocation}
+          ${concatMapStringsSep "\n" listenString vhost.listen}
+          server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
+          ${optionalString vhost.enableACME acmeLocation}
           ${optionalString (vhost.root != null) "root ${vhost.root};"}
           ${optionalString (vhost.globalRedirect != null) ''
             return 301 http${optionalString ssl "s"}://${vhost.globalRedirect}$request_uri;
@@ -380,7 +385,7 @@ in
 
       virtualHosts = mkOption {
         type = types.attrsOf (types.submodule (import ./vhost-options.nix {
-          inherit lib;
+          inherit config lib;
         }));
         default = {
           localhost = {};
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index c0ea645b3dfe..60260512bc2f 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -3,7 +3,7 @@
 # has additional options that affect the web server as a whole, like
 # the user/group to run under.)
 
-{ lib }:
+{ config, lib }:
 
 with lib;
 {
@@ -26,12 +26,26 @@ with lib;
       '';
     };
 
-    port = mkOption {
-      type = types.nullOr types.int;
-      default = null;
+    listen = mkOption {
+      type = with types; listOf (submodule {
+        options = {
+          addr = mkOption { type = str; description = "IP address."; };
+          port = mkOption { type = nullOr int; description = "Port number."; };
+        };
+      });
+      default =
+        [ { addr = "0.0.0.0"; port = null; } ]
+        ++ optional config.networking.enableIPv6
+          { addr = "[::]"; port = null; };
+      example = [
+        { addr = "195.154.1.1"; port = 443; }
+        { addr = "192.168.1.2"; port = 443; }
+      ];
       description = ''
-        Port for the server. Defaults to 80 for http
-        and 443 for https (i.e. when enableSSL is set).
+        Listen addresses and ports for this virtual host.
+        IPv6 addresses must be enclosed in square brackets.
+        Setting the port to <literal>null</literal> defaults
+        to 80 for http and 443 for https (i.e. when enableSSL is set).
       '';
     };
 
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 9a125dcb0aeb..1f4ab3eae07e 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -301,6 +301,7 @@ mountFS() {
         *x-nixos.autoresize*)
             if [ "$fsType" = ext2 -o "$fsType" = ext3 -o "$fsType" = ext4 ]; then
                 echo "resizing $device..."
+                e2fsck -fp "$device"
                 resize2fs "$device"
             fi
             ;;
diff --git a/nixos/release.nix b/nixos/release.nix
index 467e3bb8cd61..0dbdadf97816 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -303,6 +303,7 @@ in rec {
   tests.simple = callTest tests/simple.nix {};
   tests.slim = callTest tests/slim.nix {};
   tests.smokeping = callTest tests/smokeping.nix {};
+  tests.snapper = callTest tests/snapper.nix {};
   tests.taskserver = callTest tests/taskserver.nix {};
   tests.tomcat = callTest tests/tomcat.nix {};
   tests.udisks2 = callTest tests/udisks2.nix {};
diff --git a/nixos/tests/snapper.nix b/nixos/tests/snapper.nix
new file mode 100644
index 000000000000..74ec22fd3499
--- /dev/null
+++ b/nixos/tests/snapper.nix
@@ -0,0 +1,43 @@
+import ./make-test.nix ({ ... }:
+{
+  name = "snapper";
+
+  machine = { pkgs, lib, ... }: {
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux /dev/vdb
+    '';
+
+    virtualisation.emptyDiskImages = [ 4096 ];
+
+    fileSystems = lib.mkVMOverride {
+      "/home" = {
+        device = "/dev/disk/by-label/aux";
+        fsType = "btrfs";
+      };
+    };
+    services.snapper.configs.home.subvolume = "/home";
+    services.snapper.filters = "/nix";
+  };
+
+  testScript = ''
+    $machine->succeed("btrfs subvolume create /home/.snapshots");
+
+    $machine->succeed("snapper -c home list");
+
+    $machine->succeed("snapper -c home create --description empty");
+
+    $machine->succeed("echo test > /home/file");
+    $machine->succeed("snapper -c home create --description file");
+
+    $machine->succeed("snapper -c home status 1..2");
+
+    $machine->succeed("snapper -c home undochange 1..2");
+    $machine->fail("ls /home/file");
+
+    $machine->succeed("snapper -c home delete 2");
+
+    $machine->succeed("systemctl --wait start snapper-timeline.service");
+
+    $machine->succeed("systemctl --wait start snapper-cleanup.service");
+  '';
+})
diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix
index cdccb11d8887..75be97a507d0 100644
--- a/nixos/tests/taskserver.nix
+++ b/nixos/tests/taskserver.nix
@@ -246,6 +246,10 @@ in {
     };
 
     subtest "check manual configuration", sub {
+      # Remove the keys from automatic CA creation, to make sure the new
+      # generation doesn't use keys from before.
+      $server->succeed('rm -rf ${cfg.dataDir}/keys/* >&2');
+
       $server->succeed('${switchToNewServer} >&2');
       $server->waitForUnit("taskserver.service");
       $server->waitForOpenPort(${portStr});