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-1703.xml8
-rw-r--r--nixos/modules/config/pulseaudio.nix18
-rw-r--r--nixos/modules/hardware/ckb.nix40
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/programs/chromium.nix85
-rw-r--r--nixos/modules/rename.nix3
-rw-r--r--nixos/modules/services/cluster/kubernetes.nix2
-rw-r--r--nixos/modules/services/hardware/udev.nix5
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix17
-rw-r--r--nixos/modules/services/networking/dhcpd.nix250
-rw-r--r--nixos/modules/services/networking/firewall.nix9
-rw-r--r--nixos/modules/services/web-servers/caddy.nix9
-rw-r--r--nixos/tests/kubernetes.nix1
-rw-r--r--nixos/tests/networking.nix58
14 files changed, 390 insertions, 116 deletions
diff --git a/nixos/doc/manual/release-notes/rl-1703.xml b/nixos/doc/manual/release-notes/rl-1703.xml
index d8b0ae01e3335..6997155d94d05 100644
--- a/nixos/doc/manual/release-notes/rl-1703.xml
+++ b/nixos/doc/manual/release-notes/rl-1703.xml
@@ -30,6 +30,14 @@ following incompatible changes:</para>
 <itemizedlist>
   <listitem>
     <para>
+      <literal>stdenv.overrides</literal> is now expected to take <literal>self</literal>
+      and <literal>super</literal> arguments. See <literal>lib.trivial.extends</literal>
+      for what those parameters represent.
+    </para>
+  </listitem>
+
+  <listitem>
+    <para>
       <literal>gnome</literal> alias has been removed along with
       <literal>gtk</literal>, <literal>gtkmm</literal> and several others.
       Now you need to use versioned attributes, like <literal>gnome3</literal>.
diff --git a/nixos/modules/config/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
index 742167fbf69f5..d5cb4fce0f9d3 100644
--- a/nixos/modules/config/pulseaudio.nix
+++ b/nixos/modules/config/pulseaudio.nix
@@ -160,6 +160,13 @@ in {
             if activated.
           '';
         };
+
+        config = mkOption {
+          type = types.attrsOf types.unspecified;
+          default = {};
+          description = ''Config of the pulse daemon. See <literal>man pulse-daemon.conf</literal>.'';
+          example = literalExample ''{ flat-volumes = "no"; }'';
+        };
       };
 
       zeroconf = {
@@ -204,10 +211,13 @@ in {
     (mkIf cfg.enable {
       environment.systemPackages = [ overriddenPackage ];
 
-      environment.etc = singleton {
-        target = "asound.conf";
-        source = alsaConf;
-      };
+      environment.etc = [
+        { target = "asound.conf";
+          source = alsaConf; }
+
+        { target = "pulse/daemon.conf";
+          source = writeText "daemon.conf" (lib.generators.toKeyValue {} cfg.daemon.config); }
+      ];
 
       # Allow PulseAudio to get realtime priority using rtkit.
       security.rtkit.enable = true;
diff --git a/nixos/modules/hardware/ckb.nix b/nixos/modules/hardware/ckb.nix
new file mode 100644
index 0000000000000..8429572a88226
--- /dev/null
+++ b/nixos/modules/hardware/ckb.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.ckb;
+
+in
+  {
+    options.hardware.ckb = {
+      enable = mkEnableOption "the Corsair keyboard/mouse driver";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ckb;
+        defaultText = "pkgs.ckb";
+        description = ''
+          The package implementing the Corsair keyboard/mouse driver.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ cfg.package ];
+
+      systemd.services.ckb = {
+        description = "Corsair Keyboard Daemon";
+        wantedBy = ["multi-user.target"];
+        script = "${cfg.package}/bin/ckb-daemon";
+        serviceConfig = {
+          Restart = "always";
+          StandardOutput = "syslog";
+        };
+      };
+    };
+
+    meta = {
+      maintainers = with lib.maintainers; [ kierdavis ];
+    };
+  }
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index df08603499531..f76852793d465 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -66,6 +66,7 @@
   ./programs/bash/bash.nix
   ./programs/blcr.nix
   ./programs/cdemu.nix
+  ./programs/chromium.nix
   ./programs/command-not-found/command-not-found.nix
   ./programs/dconf.nix
   ./programs/environment.nix
diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix
new file mode 100644
index 0000000000000..54739feab9765
--- /dev/null
+++ b/nixos/modules/programs/chromium.nix
@@ -0,0 +1,85 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.chromium;
+
+  defaultProfile = filterAttrs (k: v: v != null) {
+    HomepageLocation = cfg.homepageLocation;
+    DefaultSearchProviderSearchURL = cfg.defaultSearchProviderSearchURL;
+    DefaultSearchProviderSuggestURL = cfg.defaultSearchProviderSuggestURL;
+    ExtensionInstallForcelist = map (extension:
+      "${extension};https://clients2.google.com/service/update2/crx"
+    ) cfg.extensions;
+  };
+in
+
+{
+  ###### interface
+
+  options = {
+    programs.chromium = {
+      enable = mkEnableOption "<command>chromium</command> policies";
+
+      extensions = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of chromium extensions to install.
+          For list of plugins ids see id in url of extensions on
+          <link xlink:href="https://chrome.google.com/webstore/category/extensions">chrome web store</link>
+          page.
+        '';
+        default = [];
+        example = literalExample ''
+          [
+            "chlffgpmiacpedhhbkiomidkjlcfhogd" # pushbullet
+            "mbniclmhobmnbdlbpiphghaielnnpgdp" # lightshot
+            "gcbommkclmclpchllfjekcdonpmejbdp" # https everywhere
+          ]
+        '';
+      };
+
+      homepageLocation = mkOption {
+        type = types.nullOr types.str;
+        description = "Chromium default homepage";
+        default = null;
+        example = "https://nixos.org";
+      };
+
+      defaultSearchProviderSearchURL = mkOption {
+        type = types.nullOr types.str;
+        description = "Chromium default search provider url.";
+        default = null;
+        example =
+          "https://encrypted.google.com/search?q={searchTerms}&{google:RLZ}{google:originalQueryForSuggestion}{google:assistedQueryStats}{google:searchFieldtrialParameter}{google:
+        ↪searchClient}{google:sourceId}{google:instantExtendedEnabledParameter}ie={inputEncoding}";
+      };
+
+      defaultSearchProviderSuggestURL = mkOption {
+        type = types.nullOr types.str;
+        description = "Chromium default search provider url for suggestions.";
+        default = null;
+        example =
+          "https://encrypted.google.com/complete/search?output=chrome&q={searchTerms}";
+      };
+
+      extraOpts = mkOption {
+        type = types.attrs;
+        description = ''
+          Extra chromium policy options, see
+          <link xlink:href="https://www.chromium.org/administrators/policy-list-3">https://www.chromium.org/administrators/policy-list-3</link>
+          for a list of avalible options
+        '';
+        default = {};
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+    environment.etc."chromium/policies/managed/default.json".text = builtins.toJSON defaultProfile;
+    environment.etc."chromium/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
+  };
+}
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 758f229d59d78..ad1ba86980d5e 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -164,6 +164,9 @@ with lib;
         else { addr = value inetAddr; port = value inetPort; }
     ))
 
+    # dhcpd
+    (mkRenamedOptionModule [ "services" "dhcpd" ] [ "services" "dhcpd4" ])
+
     # Options that are obsolete and have no replacement.
     (mkRemovedOptionModule [ "boot" "initrd" "luks" "enable" ] "")
     (mkRemovedOptionModule [ "programs" "bash" "enable" ] "")
diff --git a/nixos/modules/services/cluster/kubernetes.nix b/nixos/modules/services/cluster/kubernetes.nix
index fbf7412a6cd92..029b11ad98b7e 100644
--- a/nixos/modules/services/cluster/kubernetes.nix
+++ b/nixos/modules/services/cluster/kubernetes.nix
@@ -737,6 +737,8 @@ in {
         wantedBy = [ "multi-user.target" ];
         after = [ "kube-apiserver.service" ];
         serviceConfig = {
+          RestartSec = "30s";
+          Restart = "on-failure";
           ExecStart = ''${cfg.package}/bin/kube-controller-manager \
             --address=${cfg.controllerManager.address} \
             --port=${toString cfg.controllerManager.port} \
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 14d65978c3204..028907693a5af 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -143,7 +143,10 @@ let
       done
 
       echo "Generating hwdb database..."
-      ${udev}/bin/udevadm hwdb --update --root=$(pwd)
+      # hwdb --update doesn't return error code even on errors!
+      res="$(${udev}/bin/udevadm hwdb --update --root=$(pwd) 2>&1)"
+      echo "$res"
+      [ -z "$(echo "$res" | egrep '^Error')" ]
       mv etc/udev/hwdb.bin $out
     '';
 
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index da2cd02eaa3b2..cf761edad9260 100644
--- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -5,6 +5,10 @@ with lib;
 let
   cfg = config.services.prometheus.alertmanager;
   mkConfigFile = pkgs.writeText "alertmanager.yml" (builtins.toJSON cfg.configuration);
+  alertmanagerYml =
+    if cfg.configText != null then
+      pkgs.writeText "alertmanager.yml" cfg.configText
+    else mkConfigFile;
 in {
   options = {
     services.prometheus.alertmanager = {
@@ -34,6 +38,17 @@ in {
         '';
       };
 
+      configText = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Alertmanager configuration as YAML text. If non-null, this option
+          defines the text that is written to alertmanager.yml. If null, the
+          contents of alertmanager.yml is generated from the structured config
+          options.
+        '';
+      };
+
       logFormat = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -96,7 +111,7 @@ in {
       after    = [ "network.target" ];
       script = ''
         ${pkgs.prometheus-alertmanager.bin}/bin/alertmanager \
-        -config.file ${mkConfigFile} \
+        -config.file ${alertmanagerYml} \
         -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
         -log.level ${cfg.logLevel} \
         ${optionalString (cfg.webExternalUrl != null) ''-web.external-url ${cfg.webExternalUrl} \''}
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index d2cd00e74a1f0..86bcaa96f3454 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -4,11 +4,10 @@ with lib;
 
 let
 
-  cfg = config.services.dhcpd;
+  cfg4 = config.services.dhcpd4;
+  cfg6 = config.services.dhcpd6;
 
-  stateDir = "/var/lib/dhcp"; # Don't use /var/state/dhcp; not FHS-compliant.
-
-  configFile = if cfg.configFile != null then cfg.configFile else pkgs.writeText "dhcpd.conf"
+  writeConfig = cfg: pkgs.writeText "dhcpd.conf"
     ''
       default-lease-time 600;
       max-lease-time 7200;
@@ -29,131 +28,180 @@ let
       }
     '';
 
-in
-
-{
-
-  ###### interface
-
-  options = {
+  dhcpdService = postfix: cfg: optionalAttrs cfg.enable {
+    "dhcpd${postfix}" = {
+      description = "DHCPv${postfix} server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        mkdir -m 755 -p ${cfg.stateDir}
+        touch ${cfg.stateDir}/dhcpd.leases
+      '';
+
+      serviceConfig =
+        let
+          configFile = if cfg.configFile != null then cfg.configFile else writeConfig cfg;
+          args = [ "@${pkgs.dhcp}/sbin/dhcpd" "dhcpd${postfix}" "-${postfix}"
+                   "-pf" "/run/dhcpd${postfix}/dhcpd.pid"
+                   "-cf" "${configFile}"
+                   "-lf" "${cfg.stateDir}/dhcpd.leases"
+                   "-user" "dhcpd" "-group" "nogroup"
+                 ] ++ cfg.extraFlags
+                   ++ cfg.interfaces;
+
+        in {
+          ExecStart = concatMapStringsSep " " escapeShellArg args;
+          Type = "forking";
+          Restart = "always";
+          RuntimeDirectory = [ "dhcpd${postfix}" ];
+          PIDFile = "/run/dhcpd${postfix}/dhcpd.pid";
+        };
+    };
+  };
 
-    services.dhcpd = {
+  machineOpts = {...}: {
+    config = {
 
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to enable the DHCP server.
-        ";
+      hostName = mkOption {
+        type = types.str;
+        example = "foo";
+        description = ''
+          Hostname which is assigned statically to the machine.
+        '';
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        example = ''
-          option subnet-mask 255.255.255.0;
-          option broadcast-address 192.168.1.255;
-          option routers 192.168.1.5;
-          option domain-name-servers 130.161.158.4, 130.161.33.17, 130.161.180.1;
-          option domain-name "example.org";
-          subnet 192.168.1.0 netmask 255.255.255.0 {
-            range 192.168.1.100 192.168.1.200;
-          }
+      ethernetAddress = mkOption {
+        type = types.str;
+        example = "00:16:76:9a:32:1d";
+        description = ''
+          MAC address of the machine.
         '';
-        description = "
-          Extra text to be appended to the DHCP server configuration
-          file.  Currently, you almost certainly need to specify
-          something here, such as the options specifying the subnet
-          mask, DNS servers, etc.
-        ";
       };
 
-      extraFlags = mkOption {
-        default = "";
-        example = "-6";
-        description = "
-          Additional command line flags to be passed to the dhcpd daemon.
-        ";
+      ipAddress = mkOption {
+        type = types.str;
+        example = "192.168.1.10";
+        description = ''
+          IP address of the machine.
+        '';
       };
 
-      configFile = mkOption {
-        default = null;
-        description = "
-          The path of the DHCP server configuration file.  If no file
-          is specified, a file is generated using the other options.
-        ";
-      };
+    };
+  };
 
-      interfaces = mkOption {
-        default = ["eth0"];
-        description = "
-          The interfaces on which the DHCP server should listen.
-        ";
-      };
+  dhcpConfig = postfix: {
 
-      machines = mkOption {
-        default = [];
-        example = [
-          { hostName = "foo";
-            ethernetAddress = "00:16:76:9a:32:1d";
-            ipAddress = "192.168.1.10";
-          }
-          { hostName = "bar";
-            ethernetAddress = "00:19:d1:1d:c4:9a";
-            ipAddress = "192.168.1.11";
-          }
-        ];
-        description = "
-          A list mapping ethernet addresses to IP addresses for the
-          DHCP server.
-        ";
-      };
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable the DHCPv${postfix} server.
+      '';
+    };
 
+    stateDir = mkOption {
+      type = types.path;
+      # We use /var/lib/dhcp for DHCPv4 to save backwards compatibility.
+      default = "/var/lib/dhcp${if postfix == "4" then "" else postfix}";
+      description = ''
+        State directory for the DHCP server.
+      '';
     };
 
-  };
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        option subnet-mask 255.255.255.0;
+        option broadcast-address 192.168.1.255;
+        option routers 192.168.1.5;
+        option domain-name-servers 130.161.158.4, 130.161.33.17, 130.161.180.1;
+        option domain-name "example.org";
+        subnet 192.168.1.0 netmask 255.255.255.0 {
+          range 192.168.1.100 192.168.1.200;
+        }
+      '';
+      description = ''
+        Extra text to be appended to the DHCP server configuration
+        file. Currently, you almost certainly need to specify something
+        there, such as the options specifying the subnet mask, DNS servers,
+        etc.
+      '';
+    };
 
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Additional command line flags to be passed to the dhcpd daemon.
+      '';
+    };
 
-  ###### implementation
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        The path of the DHCP server configuration file.  If no file
+        is specified, a file is generated using the other options.
+      '';
+    };
 
-  config = mkIf config.services.dhcpd.enable {
+    interfaces = mkOption {
+      type = types.listOf types.str;
+      default = ["eth0"];
+      description = ''
+        The interfaces on which the DHCP server should listen.
+      '';
+    };
 
-    users = {
-      extraUsers.dhcpd = {
-        uid = config.ids.uids.dhcpd;
-        description = "DHCP daemon user";
-      };
+    machines = mkOption {
+      type = types.listOf (types.submodule machineOpts);
+      default = [];
+      example = [
+        { hostName = "foo";
+          ethernetAddress = "00:16:76:9a:32:1d";
+          ipAddress = "192.168.1.10";
+        }
+        { hostName = "bar";
+          ethernetAddress = "00:19:d1:1d:c4:9a";
+          ipAddress = "192.168.1.11";
+        }
+      ];
+      description = ''
+        A list mapping Ethernet addresses to IPv${postfix} addresses for the
+        DHCP server.
+      '';
     };
 
-    systemd.services.dhcpd =
-      { description = "DHCP server";
+  };
+
+in
+
+{
+
+  ###### interface
 
-        wantedBy = [ "multi-user.target" ];
+  options = {
 
-        after = [ "network.target" ];
+    services.dhcpd4 = dhcpConfig "4";
+    services.dhcpd6 = dhcpConfig "6";
 
-        path = [ pkgs.dhcp ];
+  };
 
-        preStart =
-          ''
-            mkdir -m 755 -p ${stateDir}
 
-            touch ${stateDir}/dhcpd.leases
+  ###### implementation
 
-            mkdir -m 755 -p /run/dhcpd
-            chown dhcpd /run/dhcpd
-          '';
+  config = mkIf (cfg4.enable || cfg6.enable) {
 
-        serviceConfig =
-          { ExecStart = "@${pkgs.dhcp}/sbin/dhcpd dhcpd"
-              + " -pf /run/dhcpd/dhcpd.pid -cf ${configFile}"
-              + " -lf ${stateDir}/dhcpd.leases -user dhcpd -group nogroup"
-              + " ${cfg.extraFlags}"
-              + " ${toString cfg.interfaces}";
-            Restart = "always";
-            Type = "forking";
-            PIDFile = "/run/dhcpd/dhcpd.pid";
-          };
+    users = {
+      extraUsers.dhcpd = {
+        uid = config.ids.uids.dhcpd;
+        description = "DHCP daemon user";
       };
+    };
+
+    systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
 
   };
 
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 1c0ea5034df3d..ea406864fd3f7 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -172,13 +172,16 @@ let
       }-j nixos-fw-accept
     ''}
 
-    # Accept all ICMPv6 messages except redirects and node
-    # information queries (type 139).  See RFC 4890, section
-    # 4.4.
     ${optionalString config.networking.enableIPv6 ''
+      # Accept all ICMPv6 messages except redirects and node
+      # information queries (type 139).  See RFC 4890, section
+      # 4.4.
       ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
       ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
       ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
+
+      # Allow this host to act as a DHCPv6 client
+      ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
     ''}
 
     ${cfg.extraCommands}
diff --git a/nixos/modules/services/web-servers/caddy.nix b/nixos/modules/services/web-servers/caddy.nix
index 0666dfddaffd1..619e0f90b1247 100644
--- a/nixos/modules/services/web-servers/caddy.nix
+++ b/nixos/modules/services/web-servers/caddy.nix
@@ -39,6 +39,13 @@ in
       type = types.path;
       description = "The data directory, for storing certificates.";
     };
+
+    package = mkOption {
+      default = pkgs.caddy;
+      defaultText = "pkgs.caddy";
+      type = types.package;
+      description = "Caddy package to use.";
+    };
   };
 
   config = mkIf cfg.enable {
@@ -47,7 +54,7 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = ''${pkgs.caddy.bin}/bin/caddy -conf=${configFile} \
+        ExecStart = ''${cfg.package.bin}/bin/caddy -conf=${configFile} \
           -ca=${cfg.ca} -email=${cfg.email} ${optionalString cfg.agree "-agree"}
         '';
         Type = "simple";
diff --git a/nixos/tests/kubernetes.nix b/nixos/tests/kubernetes.nix
index 273bd3c80c192..dcd25e211971b 100644
--- a/nixos/tests/kubernetes.nix
+++ b/nixos/tests/kubernetes.nix
@@ -59,6 +59,7 @@ in {
             virtualisation.diskSize = 2048;
 
             programs.bash.enableCompletion = true;
+            environment.systemPackages = with pkgs; [ netcat bind ];
 
             services.kubernetes.roles = ["master" "node"];
             virtualisation.docker.extraOptions = "--iptables=false --ip-masq=false -b cbr0";
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index 17d4a878d3a4f..83103f35d4821 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -10,29 +10,61 @@ let
       vlanIfs = range 1 (length config.virtualisation.vlans);
     in {
       virtualisation.vlans = [ 1 2 3 ];
+      boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
       networking = {
         useDHCP = false;
         useNetworkd = networkd;
         firewall.allowPing = true;
+        firewall.checkReversePath = true;
+        firewall.allowedUDPPorts = [ 547 ];
         interfaces = mkOverride 0 (listToAttrs (flip map vlanIfs (n:
           nameValuePair "eth${toString n}" {
             ipAddress = "192.168.${toString n}.1";
             prefixLength = 24;
+            ipv6Address = "fd00:1234:5678:${toString n}::1";
+            ipv6PrefixLength = 64;
           })));
       };
-      services.dhcpd = {
+      services.dhcpd4 = {
         enable = true;
         interfaces = map (n: "eth${toString n}") vlanIfs;
         extraConfig = ''
-          option subnet-mask 255.255.255.0;
+          authoritative;
         '' + flip concatMapStrings vlanIfs (n: ''
           subnet 192.168.${toString n}.0 netmask 255.255.255.0 {
-            option broadcast-address 192.168.${toString n}.255;
             option routers 192.168.${toString n}.1;
+            # XXX: technically it's _not guaranteed_ that IP addresses will be
+            # issued from the first item in range onwards! We assume that in
+            # our tests however.
             range 192.168.${toString n}.2 192.168.${toString n}.254;
           }
         '');
       };
+      services.radvd = {
+        enable = true;
+        config = flip concatMapStrings vlanIfs (n: ''
+          interface eth${toString n} {
+            AdvSendAdvert on;
+            AdvManagedFlag on;
+            AdvOtherConfigFlag on;
+
+            prefix fd00:1234:5678:${toString n}::/64 {
+              AdvAutonomous off;
+            };
+          };
+        '');
+      };
+      services.dhcpd6 = {
+        enable = true;
+        interfaces = map (n: "eth${toString n}") vlanIfs;
+        extraConfig = ''
+          authoritative;
+        '' + flip concatMapStrings vlanIfs (n: ''
+          subnet6 fd00:1234:5678:${toString n}::/64 {
+            range6 fd00:1234:5678:${toString n}::2 fd00:1234:5678:${toString n}::2;
+          }
+        '');
+      };
     };
 
   testCases = {
@@ -108,8 +140,14 @@ let
           useNetworkd = networkd;
           firewall.allowPing = true;
           useDHCP = true;
-          interfaces.eth1.ip4 = mkOverride 0 [ ];
-          interfaces.eth2.ip4 = mkOverride 0 [ ];
+          interfaces.eth1 = {
+            ip4 = mkOverride 0 [ ];
+            ip6 = mkOverride 0 [ ];
+          };
+          interfaces.eth2 = {
+            ip4 = mkOverride 0 [ ];
+            ip6 = mkOverride 0 [ ];
+          };
         };
       };
       testScript = { nodes, ... }:
@@ -121,21 +159,31 @@ let
 
           # Wait until we have an ip address on each interface
           $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q '192.168.1'");
+          $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'");
           $client->waitUntilSucceeds("ip addr show dev eth2 | grep -q '192.168.2'");
+          $client->waitUntilSucceeds("ip addr show dev eth2 | grep -q 'fd00:1234:5678:2:'");
 
           # Test vlan 1
           $client->waitUntilSucceeds("ping -c 1 192.168.1.1");
           $client->waitUntilSucceeds("ping -c 1 192.168.1.2");
+          $client->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:1::1");
+          $client->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:1::2");
 
           $router->waitUntilSucceeds("ping -c 1 192.168.1.1");
           $router->waitUntilSucceeds("ping -c 1 192.168.1.2");
+          $router->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:1::1");
+          $router->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:1::2");
 
           # Test vlan 2
           $client->waitUntilSucceeds("ping -c 1 192.168.2.1");
           $client->waitUntilSucceeds("ping -c 1 192.168.2.2");
+          $client->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:2::1");
+          $client->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:2::2");
 
           $router->waitUntilSucceeds("ping -c 1 192.168.2.1");
           $router->waitUntilSucceeds("ping -c 1 192.168.2.2");
+          $router->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:2::1");
+          $router->waitUntilSucceeds("ping6 -c 1 fd00:1234:5678:2::2");
         '';
     };
     dhcpOneIf = {