lol

nixos/network: allow configuring tempaddr for undeclared interfaces

+121 -54
+61 -33
nixos/modules/tasks/network-interfaces.nix
··· 144 144 }; 145 145 146 146 tempAddress = mkOption { 147 - type = types.enum [ "default" "enabled" "disabled" ]; 148 - default = if cfg.enableIPv6 then "default" else "disabled"; 149 - defaultText = literalExample ''if cfg.enableIPv6 then "default" else "disabled"''; 147 + type = types.enum (lib.attrNames tempaddrValues); 148 + default = cfg.tempAddresses; 149 + defaultText = literalExample ''config.networking.tempAddresses''; 150 150 description = '' 151 151 When IPv6 is enabled with SLAAC, this option controls the use of 152 - temporary address (aka privacy extensions). This is used to reduce tracking. 153 - The three possible values are: 152 + temporary address (aka privacy extensions) on this 153 + interface. This is used to reduce tracking. 154 + 155 + See also the global option 156 + <xref linkend="opt-networking.tempAddresses"/>, which 157 + applies to all interfaces where this is not set. 154 158 155 - <itemizedlist> 156 - <listitem> 157 - <para> 158 - <literal>"default"</literal> to generate temporary addresses and use 159 - them by default; 160 - </para> 161 - </listitem> 162 - <listitem> 163 - <para> 164 - <literal>"enabled"</literal> to generate temporary addresses but keep 165 - using the standard EUI-64 ones by default; 166 - </para> 167 - </listitem> 168 - <listitem> 169 - <para> 170 - <literal>"disabled"</literal> to completely disable temporary addresses. 171 - </para> 172 - </listitem> 173 - </itemizedlist> 159 + Possible values are: 160 + ${tempaddrDoc} 174 161 ''; 175 162 }; 176 163 ··· 365 352 hexChars = stringToCharacters "0123456789abcdef"; 366 353 367 354 isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s)); 355 + 356 + tempaddrValues = { 357 + disabled = { 358 + sysctl = "0"; 359 + description = "completely disable IPv6 temporary addresses"; 360 + }; 361 + enabled = { 362 + sysctl = "1"; 363 + description = "generate IPv6 temporary addresses but still use EUI-64 addresses as source addresses"; 364 + }; 365 + default = { 366 + sysctl = "2"; 367 + description = "generate IPv6 temporary addresses and use these as source addresses in routing"; 368 + }; 369 + }; 370 + tempaddrDoc = '' 371 + <itemizedlist> 372 + ${concatStringsSep "\n" (mapAttrsToList (name: { description, ... }: '' 373 + <listitem> 374 + <para> 375 + <literal>"${name}"</literal> to ${description}; 376 + </para> 377 + </listitem> 378 + '') tempaddrValues)} 379 + </itemizedlist> 380 + ''; 368 381 369 382 in 370 383 ··· 1039 1052 ''; 1040 1053 }; 1041 1054 1055 + networking.tempAddresses = mkOption { 1056 + default = if cfg.enableIPv6 then "default" else "disabled"; 1057 + type = types.enum (lib.attrNames tempaddrValues); 1058 + description = '' 1059 + Whether to enable IPv6 Privacy Extensions for interfaces not 1060 + configured explicitly in 1061 + <xref linkend="opt-networking.interfaces._name_.tempAddress" />. 1062 + 1063 + This sets the ipv6.conf.*.use_tempaddr sysctl for all 1064 + interfaces. Possible values are: 1065 + 1066 + ${tempaddrDoc} 1067 + ''; 1068 + }; 1069 + 1042 1070 }; 1043 1071 1044 1072 ··· 1098 1126 // listToAttrs (forEach interfaces 1099 1127 (i: let 1100 1128 opt = i.tempAddress; 1101 - val = { disabled = 0; enabled = 1; default = 2; }.${opt}; 1129 + val = tempaddrValues.${opt}.sysctl; 1102 1130 in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val)); 1103 1131 1104 1132 # Capabilities won't work unless we have at-least a 4.3 Linux ··· 1188 1216 (pkgs.writeTextFile rec { 1189 1217 name = "ipv6-privacy-extensions.rules"; 1190 1218 destination = "/etc/udev/rules.d/98-${name}"; 1191 - text = '' 1219 + text = let 1220 + sysctl-value = tempaddrValues.${cfg.tempAddresses}.sysctl; 1221 + in '' 1192 1222 # enable and prefer IPv6 privacy addresses by default 1193 - ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo 2 > /proc/sys/net/ipv6/conf/%k/use_tempaddr'" 1223 + ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/%k/use_tempaddr'" 1194 1224 ''; 1195 1225 }) 1196 1226 (pkgs.writeTextFile rec { ··· 1199 1229 text = concatMapStrings (i: 1200 1230 let 1201 1231 opt = i.tempAddress; 1202 - val = if opt == "disabled" then 0 else 1; 1203 - msg = if opt == "disabled" 1204 - then "completely disable IPv6 privacy addresses" 1205 - else "enable IPv6 privacy addresses but prefer EUI-64 addresses"; 1232 + val = tempaddrValues.${opt}.sysctl; 1233 + msg = tempaddrValues.${opt}.description; 1206 1234 in 1207 1235 '' 1208 1236 # override to ${msg} for ${i.name} 1209 - ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${toString val}" 1210 - '') (filter (i: i.tempAddress != "default") interfaces); 1237 + ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${val}" 1238 + '') (filter (i: i.tempAddress != cfg.tempAddresses) interfaces); 1211 1239 }) 1212 1240 ] ++ lib.optional (cfg.wlanInterfaces != {}) 1213 1241 (pkgs.writeTextFile {
+60 -21
nixos/tests/ipv6.nix
··· 8 8 }; 9 9 10 10 nodes = 11 - # Remove the interface configuration provided by makeTest so that the 12 - # interfaces are all configured implicitly 13 - { client = { ... }: { networking.interfaces = lib.mkForce {}; }; 11 + { 12 + # We use lib.mkForce here to remove the interface configuration 13 + # provided by makeTest, so that the interfaces are all configured 14 + # implicitly. 15 + 16 + # This client should use privacy extensions fully, having a 17 + # completely-default network configuration. 18 + client_defaults.networking.interfaces = lib.mkForce {}; 19 + 20 + # Both of these clients should obtain temporary addresses, but 21 + # not use them as the default source IP. We thus run the same 22 + # checks against them — but the configuration resulting in this 23 + # behaviour is different. 24 + 25 + # Here, by using an altered default value for the global setting... 26 + client_global_setting = { 27 + networking.interfaces = lib.mkForce {}; 28 + networking.tempAddresses = "enabled"; 29 + }; 30 + # and here, by setting this on the interface explicitly. 31 + client_interface_setting = { 32 + networking.tempAddresses = "disabled"; 33 + networking.interfaces = lib.mkForce { 34 + eth1.tempAddress = "enabled"; 35 + }; 36 + }; 14 37 15 38 server = 16 - { ... }: 17 39 { services.httpd.enable = true; 18 40 services.httpd.adminAddr = "foo@example.org"; 19 41 networking.firewall.allowedTCPPorts = [ 80 ]; ··· 40 62 # Start the router first so that it respond to router solicitations. 41 63 router.wait_for_unit("radvd") 42 64 65 + clients = [client_defaults, client_global_setting, client_interface_setting] 66 + 43 67 start_all() 44 68 45 - client.wait_for_unit("network.target") 69 + for client in clients: 70 + client.wait_for_unit("network.target") 46 71 server.wait_for_unit("network.target") 47 72 server.wait_for_unit("httpd.service") 48 73 ··· 64 89 65 90 66 91 with subtest("Loopback address can be pinged"): 67 - client.succeed("ping -c 1 ::1 >&2") 68 - client.fail("ping -c 1 ::2 >&2") 92 + client_defaults.succeed("ping -c 1 ::1 >&2") 93 + client_defaults.fail("ping -c 1 2001:db8:: >&2") 69 94 70 95 with subtest("Local link addresses can be obtained and pinged"): 71 - client_ip = wait_for_address(client, "eth1", "link") 72 - server_ip = wait_for_address(server, "eth1", "link") 73 - client.succeed(f"ping -c 1 {client_ip}%eth1 >&2") 74 - client.succeed(f"ping -c 1 {server_ip}%eth1 >&2") 96 + for client in clients: 97 + client_ip = wait_for_address(client, "eth1", "link") 98 + server_ip = wait_for_address(server, "eth1", "link") 99 + client.succeed(f"ping -c 1 {client_ip}%eth1 >&2") 100 + client.succeed(f"ping -c 1 {server_ip}%eth1 >&2") 75 101 76 102 with subtest("Global addresses can be obtained, pinged, and reached via http"): 77 - client_ip = wait_for_address(client, "eth1", "global") 78 - server_ip = wait_for_address(server, "eth1", "global") 79 - client.succeed(f"ping -c 1 {client_ip} >&2") 80 - client.succeed(f"ping -c 1 {server_ip} >&2") 81 - client.succeed(f"curl --fail -g http://[{server_ip}]") 82 - client.fail(f"curl --fail -g http://[{client_ip}]") 103 + for client in clients: 104 + client_ip = wait_for_address(client, "eth1", "global") 105 + server_ip = wait_for_address(server, "eth1", "global") 106 + client.succeed(f"ping -c 1 {client_ip} >&2") 107 + client.succeed(f"ping -c 1 {server_ip} >&2") 108 + client.succeed(f"curl --fail -g http://[{server_ip}]") 109 + client.fail(f"curl --fail -g http://[{client_ip}]") 83 110 84 - with subtest("Privacy extensions: Global temporary address can be obtained and pinged"): 85 - ip = wait_for_address(client, "eth1", "global", temporary=True) 111 + with subtest( 112 + "Privacy extensions: Global temporary address is used as default source address" 113 + ): 114 + ip = wait_for_address(client_defaults, "eth1", "global", temporary=True) 86 115 # Default route should have "src <temporary address>" in it 87 - client.succeed(f"ip r g ::2 | grep {ip}") 116 + client_defaults.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'") 88 117 89 - # TODO: test reachability of a machine on another network. 118 + for client, setting_desc in ( 119 + (client_global_setting, "global"), 120 + (client_interface_setting, "interface"), 121 + ): 122 + with subtest(f'Privacy extensions: "enabled" through {setting_desc} setting)'): 123 + # We should be obtaining both a temporary address and an EUI-64 address... 124 + ip = wait_for_address(client, "eth1", "global") 125 + assert "ff:fe" in ip 126 + ip_temp = wait_for_address(client, "eth1", "global", temporary=True) 127 + # But using the EUI-64 one. 128 + client.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'") 90 129 ''; 91 130 })