Merge pull request #118037 from mayflower/privacy-extensions-configurable

nixos/network: allow configuring tempaddr for undeclared interfaces
This commit is contained in:
Robin Gloster 2021-05-07 13:01:29 -05:00 committed by GitHub
commit 29e92116d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 56 deletions

View File

@ -7,8 +7,12 @@
<para> <para>
IPv6 is enabled by default. Stateless address autoconfiguration is used to IPv6 is enabled by default. Stateless address autoconfiguration is used to
automatically assign IPv6 addresses to all interfaces. You can disable IPv6 automatically assign IPv6 addresses to all interfaces, and Privacy
support globally by setting: Extensions (RFC 4946) are enabled by default. You can adjust the default
for this by setting <xref linkend="opt-networking.tempAddresses"/>.
This option may be overridden on a per-interface basis by
<xref linkend="opt-networking.interfaces._name_.tempAddress"/>.
You can disable IPv6 support globally by setting:
<programlisting> <programlisting>
<xref linkend="opt-networking.enableIPv6"/> = false; <xref linkend="opt-networking.enableIPv6"/> = false;
</programlisting> </programlisting>

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}'")
''; '';
}) })