diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index f501f85b2a92..3d1628d0783d 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 f9d6d82b54ac..75faa6f60201 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}'")
'';
})