nixos/network: allow configuring tempaddr for undeclared interfaces

This commit is contained in:
Linus Heckemann 2021-03-30 11:49:13 +02:00
parent 1fe6ed37fd
commit 4c4ac4bb20
2 changed files with 121 additions and 54 deletions

View File

@ -144,33 +144,20 @@ let
}; };
tempAddress = mkOption { tempAddress = mkOption {
type = types.enum [ "default" "enabled" "disabled" ]; type = types.enum (lib.attrNames tempaddrValues);
default = if cfg.enableIPv6 then "default" else "disabled"; default = cfg.tempAddresses;
defaultText = literalExample ''if cfg.enableIPv6 then "default" else "disabled"''; defaultText = literalExample ''config.networking.tempAddresses'';
description = '' description = ''
When IPv6 is enabled with SLAAC, this option controls the use of When IPv6 is enabled with SLAAC, this option controls the use of
temporary address (aka privacy extensions). This is used to reduce tracking. temporary address (aka privacy extensions) on this
The three possible values are: interface. This is used to reduce tracking.
<itemizedlist> See also the global option
<listitem> <xref linkend="opt-networking.tempAddresses"/>, which
<para> applies to all interfaces where this is not set.
<literal>"default"</literal> to generate temporary addresses and use
them by default; Possible values are:
</para> ${tempaddrDoc}
</listitem>
<listitem>
<para>
<literal>"enabled"</literal> to generate temporary addresses but keep
using the standard EUI-64 ones by default;
</para>
</listitem>
<listitem>
<para>
<literal>"disabled"</literal> to completely disable temporary addresses.
</para>
</listitem>
</itemizedlist>
''; '';
}; };
@ -366,6 +353,32 @@ let
isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s)); 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 = ''
<itemizedlist>
${concatStringsSep "\n" (mapAttrsToList (name: { description, ... }: ''
<listitem>
<para>
<literal>"${name}"</literal> to ${description};
</para>
</listitem>
'') tempaddrValues)}
</itemizedlist>
'';
in 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
<xref linkend="opt-networking.interfaces._name_.tempAddress" />.
This sets the ipv6.conf.*.use_tempaddr sysctl for all
interfaces. Possible values are:
${tempaddrDoc}
'';
};
}; };
@ -1098,7 +1126,7 @@ in
// listToAttrs (forEach interfaces // listToAttrs (forEach interfaces
(i: let (i: let
opt = i.tempAddress; 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)); in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val));
# Capabilities won't work unless we have at-least a 4.3 Linux # Capabilities won't work unless we have at-least a 4.3 Linux
@ -1188,9 +1216,11 @@ in
(pkgs.writeTextFile rec { (pkgs.writeTextFile rec {
name = "ipv6-privacy-extensions.rules"; name = "ipv6-privacy-extensions.rules";
destination = "/etc/udev/rules.d/98-${name}"; 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 # 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 { (pkgs.writeTextFile rec {
@ -1199,15 +1229,13 @@ in
text = concatMapStrings (i: text = concatMapStrings (i:
let let
opt = i.tempAddress; opt = i.tempAddress;
val = if opt == "disabled" then 0 else 1; val = tempaddrValues.${opt}.sysctl;
msg = if opt == "disabled" msg = tempaddrValues.${opt}.description;
then "completely disable IPv6 privacy addresses"
else "enable IPv6 privacy addresses but prefer EUI-64 addresses";
in in
'' ''
# override to ${msg} for ${i.name} # 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}" ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${val}"
'') (filter (i: i.tempAddress != "default") interfaces); '') (filter (i: i.tempAddress != cfg.tempAddresses) interfaces);
}) })
] ++ lib.optional (cfg.wlanInterfaces != {}) ] ++ lib.optional (cfg.wlanInterfaces != {})
(pkgs.writeTextFile { (pkgs.writeTextFile {

View File

@ -8,12 +8,34 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
}; };
nodes = nodes =
# Remove the interface configuration provided by makeTest so that the {
# interfaces are all configured implicitly # We use lib.mkForce here to remove the interface configuration
{ client = { ... }: { networking.interfaces = lib.mkForce {}; }; # 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 = server =
{ ... }:
{ services.httpd.enable = true; { services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org"; services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ]; 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. # Start the router first so that it respond to router solicitations.
router.wait_for_unit("radvd") router.wait_for_unit("radvd")
clients = [client_defaults, client_global_setting, client_interface_setting]
start_all() 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("network.target")
server.wait_for_unit("httpd.service") server.wait_for_unit("httpd.service")
@ -64,28 +89,42 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
with subtest("Loopback address can be pinged"): with subtest("Loopback address can be pinged"):
client.succeed("ping -c 1 ::1 >&2") client_defaults.succeed("ping -c 1 ::1 >&2")
client.fail("ping -c 1 ::2 >&2") client_defaults.fail("ping -c 1 2001:db8:: >&2")
with subtest("Local link addresses can be obtained and pinged"): with subtest("Local link addresses can be obtained and pinged"):
client_ip = wait_for_address(client, "eth1", "link") for client in clients:
server_ip = wait_for_address(server, "eth1", "link") client_ip = wait_for_address(client, "eth1", "link")
client.succeed(f"ping -c 1 {client_ip}%eth1 >&2") server_ip = wait_for_address(server, "eth1", "link")
client.succeed(f"ping -c 1 {server_ip}%eth1 >&2") 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"): with subtest("Global addresses can be obtained, pinged, and reached via http"):
client_ip = wait_for_address(client, "eth1", "global") for client in clients:
server_ip = wait_for_address(server, "eth1", "global") client_ip = wait_for_address(client, "eth1", "global")
client.succeed(f"ping -c 1 {client_ip} >&2") server_ip = wait_for_address(server, "eth1", "global")
client.succeed(f"ping -c 1 {server_ip} >&2") client.succeed(f"ping -c 1 {client_ip} >&2")
client.succeed(f"curl --fail -g http://[{server_ip}]") client.succeed(f"ping -c 1 {server_ip} >&2")
client.fail(f"curl --fail -g http://[{client_ip}]") 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"): with subtest(
ip = wait_for_address(client, "eth1", "global", temporary=True) "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 <temporary address>" in it # Default route should have "src <temporary address>" 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}'")
''; '';
}) })