From 1158eda66a7646ca8b8204ffbb3d1c55a73d0374 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Sat, 14 Jan 2017 14:36:33 +0300 Subject: [PATCH 1/4] dhcpd service: add DHCPv6 support --- nixos/modules/rename.nix | 3 + nixos/modules/services/networking/dhcpd.nix | 262 ++++++++++++-------- 2 files changed, 158 insertions(+), 107 deletions(-) diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix index 758f229d59d7..ad1ba86980d5 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/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix index d2cd00e74a1f..86bcaa96f345 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,6 +28,154 @@ let } ''; + 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"; + }; + }; + }; + + machineOpts = {...}: { + config = { + + hostName = mkOption { + type = types.str; + example = "foo"; + description = '' + Hostname which is assigned statically to the machine. + ''; + }; + + ethernetAddress = mkOption { + type = types.str; + example = "00:16:76:9a:32:1d"; + description = '' + MAC address of the machine. + ''; + }; + + ipAddress = mkOption { + type = types.str; + example = "192.168.1.10"; + description = '' + IP address of the machine. + ''; + }; + + }; + }; + + dhcpConfig = postfix: { + + 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. + ''; + }; + + 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. + ''; + }; + + interfaces = mkOption { + type = types.listOf types.str; + default = ["eth0"]; + description = '' + The interfaces on which the DHCP server should listen. + ''; + }; + + 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. + ''; + }; + + }; + in { @@ -37,85 +184,15 @@ in options = { - services.dhcpd = { - - enable = mkOption { - default = false; - description = " - Whether to enable 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 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. - "; - }; - - 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. - "; - }; - - 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. - "; - }; - - }; + services.dhcpd4 = dhcpConfig "4"; + services.dhcpd6 = dhcpConfig "6"; }; ###### implementation - config = mkIf config.services.dhcpd.enable { + config = mkIf (cfg4.enable || cfg6.enable) { users = { extraUsers.dhcpd = { @@ -124,36 +201,7 @@ in }; }; - systemd.services.dhcpd = - { description = "DHCP server"; - - wantedBy = [ "multi-user.target" ]; - - after = [ "network.target" ]; - - path = [ pkgs.dhcp ]; - - preStart = - '' - mkdir -m 755 -p ${stateDir} - - touch ${stateDir}/dhcpd.leases - - mkdir -m 755 -p /run/dhcpd - chown dhcpd /run/dhcpd - ''; - - 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"; - }; - }; + systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6; }; From cb418318a046abc282ab14d98f469d3e2d264a58 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Sat, 14 Jan 2017 16:18:08 +0300 Subject: [PATCH 2/4] radvd: 2.13 -> 2.15 --- pkgs/tools/networking/radvd/default.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/tools/networking/radvd/default.nix b/pkgs/tools/networking/radvd/default.nix index 1c8ef67a7830..6c74b52b45f5 100644 --- a/pkgs/tools/networking/radvd/default.nix +++ b/pkgs/tools/networking/radvd/default.nix @@ -1,16 +1,16 @@ { stdenv, fetchurl, pkgconfig, libdaemon, bison, flex, check }: stdenv.mkDerivation rec { - name = "radvd-2.13"; + name = "radvd-${version}"; + version = "2.15"; src = fetchurl { url = "http://www.litech.org/radvd/dist/${name}.tar.xz"; - sha256 = "1lzgg6zpizcldb78n5gkykjnpr7sqm4r1xy9bm4ig0chbrink4ka"; + sha256 = "09spyj4f05rjx21v8vmyqmmj0fz1wx810s63md1vf05hyzd0v8dk"; }; - buildInputs = [ pkgconfig libdaemon bison flex check ]; - - hardeningEnable = [ "pie" ]; + nativeBuildInputs = [ pkgconfig bison flex check ]; + buildInputs = [ libdaemon ]; meta = with stdenv.lib; { homepage = http://www.litech.org/radvd/; From 820b4cd067c3965f219b135adf773e3ea334774d Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Sat, 14 Jan 2017 19:01:19 +0300 Subject: [PATCH 3/4] firewall service: allow DHCPv6 client traffic --- nixos/modules/services/networking/firewall.nix | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix index 1c0ea5034df3..ea406864fd3f 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} From 86755d923b201e2e450cf08af0ec8260d00b39a9 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Sat, 14 Jan 2017 19:01:46 +0300 Subject: [PATCH 4/4] networking test: test IPv6 with RA and DHCPv6 --- nixos/tests/networking.nix | 58 ++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix index 17d4a878d3a4..83103f35d482 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 = {