diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index f501f85b2a9..3d1628d0783 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -144,33 +144,20 @@ let }; tempAddress = mkOption { - type = types.enum [ "default" "enabled" "disabled" ]; - default = if cfg.enableIPv6 then "default" else "disabled"; - defaultText = literalExample ''if cfg.enableIPv6 then "default" else "disabled"''; + type = types.enum (lib.attrNames tempaddrValues); + default = cfg.tempAddresses; + defaultText = literalExample ''config.networking.tempAddresses''; description = '' When IPv6 is enabled with SLAAC, this option controls the use of - temporary address (aka privacy extensions). This is used to reduce tracking. - The three possible values are: + temporary address (aka privacy extensions) on this + interface. This is used to reduce tracking. - - - - "default" to generate temporary addresses and use - them by default; - - - - - "enabled" to generate temporary addresses but keep - using the standard EUI-64 ones by default; - - - - - "disabled" to completely disable temporary addresses. - - - + See also the global option + , which + applies to all interfaces where this is not set. + + Possible values are: + ${tempaddrDoc} ''; }; @@ -366,6 +353,32 @@ let isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s)); + tempaddrValues = { + disabled = { + sysctl = "0"; + description = "completely disable IPv6 temporary addresses"; + }; + enabled = { + sysctl = "1"; + description = "generate IPv6 temporary addresses but still use EUI-64 addresses as source addresses"; + }; + default = { + sysctl = "2"; + description = "generate IPv6 temporary addresses and use these as source addresses in routing"; + }; + }; + tempaddrDoc = '' + + ${concatStringsSep "\n" (mapAttrsToList (name: { description, ... }: '' + + + "${name}" to ${description}; + + + '') tempaddrValues)} + + ''; + in { @@ -1039,6 +1052,21 @@ in ''; }; + networking.tempAddresses = mkOption { + default = if cfg.enableIPv6 then "default" else "disabled"; + type = types.enum (lib.attrNames tempaddrValues); + description = '' + Whether to enable IPv6 Privacy Extensions for interfaces not + configured explicitly in + . + + This sets the ipv6.conf.*.use_tempaddr sysctl for all + interfaces. Possible values are: + + ${tempaddrDoc} + ''; + }; + }; @@ -1098,7 +1126,7 @@ in // listToAttrs (forEach interfaces (i: let opt = i.tempAddress; - val = { disabled = 0; enabled = 1; default = 2; }.${opt}; + val = tempaddrValues.${opt}.sysctl; in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val)); # Capabilities won't work unless we have at-least a 4.3 Linux @@ -1188,9 +1216,11 @@ in (pkgs.writeTextFile rec { name = "ipv6-privacy-extensions.rules"; destination = "/etc/udev/rules.d/98-${name}"; - text = '' + text = let + sysctl-value = tempaddrValues.${cfg.tempAddresses}.sysctl; + in '' # enable and prefer IPv6 privacy addresses by default - ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo 2 > /proc/sys/net/ipv6/conf/%k/use_tempaddr'" + ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/%k/use_tempaddr'" ''; }) (pkgs.writeTextFile rec { @@ -1199,15 +1229,13 @@ in text = concatMapStrings (i: let opt = i.tempAddress; - val = if opt == "disabled" then 0 else 1; - msg = if opt == "disabled" - then "completely disable IPv6 privacy addresses" - else "enable IPv6 privacy addresses but prefer EUI-64 addresses"; + val = tempaddrValues.${opt}.sysctl; + msg = tempaddrValues.${opt}.description; in '' # override to ${msg} for ${i.name} - ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${toString val}" - '') (filter (i: i.tempAddress != "default") interfaces); + ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${val}" + '') (filter (i: i.tempAddress != cfg.tempAddresses) interfaces); }) ] ++ lib.optional (cfg.wlanInterfaces != {}) (pkgs.writeTextFile { diff --git a/nixos/tests/ipv6.nix b/nixos/tests/ipv6.nix index f9d6d82b54a..75faa6f6020 100644 --- a/nixos/tests/ipv6.nix +++ b/nixos/tests/ipv6.nix @@ -8,12 +8,34 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : { }; nodes = - # Remove the interface configuration provided by makeTest so that the - # interfaces are all configured implicitly - { client = { ... }: { networking.interfaces = lib.mkForce {}; }; + { + # We use lib.mkForce here to remove the interface configuration + # provided by makeTest, so that the interfaces are all configured + # implicitly. + + # This client should use privacy extensions fully, having a + # completely-default network configuration. + client_defaults.networking.interfaces = lib.mkForce {}; + + # Both of these clients should obtain temporary addresses, but + # not use them as the default source IP. We thus run the same + # checks against them — but the configuration resulting in this + # behaviour is different. + + # Here, by using an altered default value for the global setting... + client_global_setting = { + networking.interfaces = lib.mkForce {}; + networking.tempAddresses = "enabled"; + }; + # and here, by setting this on the interface explicitly. + client_interface_setting = { + networking.tempAddresses = "disabled"; + networking.interfaces = lib.mkForce { + eth1.tempAddress = "enabled"; + }; + }; server = - { ... }: { services.httpd.enable = true; services.httpd.adminAddr = "foo@example.org"; networking.firewall.allowedTCPPorts = [ 80 ]; @@ -40,9 +62,12 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : { # Start the router first so that it respond to router solicitations. router.wait_for_unit("radvd") + clients = [client_defaults, client_global_setting, client_interface_setting] + start_all() - client.wait_for_unit("network.target") + for client in clients: + client.wait_for_unit("network.target") server.wait_for_unit("network.target") server.wait_for_unit("httpd.service") @@ -64,28 +89,42 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : { with subtest("Loopback address can be pinged"): - client.succeed("ping -c 1 ::1 >&2") - client.fail("ping -c 1 ::2 >&2") + client_defaults.succeed("ping -c 1 ::1 >&2") + client_defaults.fail("ping -c 1 2001:db8:: >&2") with subtest("Local link addresses can be obtained and pinged"): - client_ip = wait_for_address(client, "eth1", "link") - server_ip = wait_for_address(server, "eth1", "link") - client.succeed(f"ping -c 1 {client_ip}%eth1 >&2") - client.succeed(f"ping -c 1 {server_ip}%eth1 >&2") + for client in clients: + client_ip = wait_for_address(client, "eth1", "link") + server_ip = wait_for_address(server, "eth1", "link") + client.succeed(f"ping -c 1 {client_ip}%eth1 >&2") + client.succeed(f"ping -c 1 {server_ip}%eth1 >&2") with subtest("Global addresses can be obtained, pinged, and reached via http"): - client_ip = wait_for_address(client, "eth1", "global") - server_ip = wait_for_address(server, "eth1", "global") - client.succeed(f"ping -c 1 {client_ip} >&2") - client.succeed(f"ping -c 1 {server_ip} >&2") - client.succeed(f"curl --fail -g http://[{server_ip}]") - client.fail(f"curl --fail -g http://[{client_ip}]") + for client in clients: + client_ip = wait_for_address(client, "eth1", "global") + server_ip = wait_for_address(server, "eth1", "global") + client.succeed(f"ping -c 1 {client_ip} >&2") + client.succeed(f"ping -c 1 {server_ip} >&2") + client.succeed(f"curl --fail -g http://[{server_ip}]") + client.fail(f"curl --fail -g http://[{client_ip}]") - with subtest("Privacy extensions: Global temporary address can be obtained and pinged"): - ip = wait_for_address(client, "eth1", "global", temporary=True) + with subtest( + "Privacy extensions: Global temporary address is used as default source address" + ): + ip = wait_for_address(client_defaults, "eth1", "global", temporary=True) # Default route should have "src " in it - client.succeed(f"ip r g ::2 | grep {ip}") + client_defaults.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'") - # TODO: test reachability of a machine on another network. + for client, setting_desc in ( + (client_global_setting, "global"), + (client_interface_setting, "interface"), + ): + with subtest(f'Privacy extensions: "enabled" through {setting_desc} setting)'): + # We should be obtaining both a temporary address and an EUI-64 address... + ip = wait_for_address(client, "eth1", "global") + assert "ff:fe" in ip + ip_temp = wait_for_address(client, "eth1", "global", temporary=True) + # But using the EUI-64 one. + client.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'") ''; })