diff --git a/nixos/doc/manual/configuration/ad-hoc-network-config.section.md b/nixos/doc/manual/configuration/ad-hoc-network-config.section.md index 7ef24825526d..611f3e3956ee 100644 --- a/nixos/doc/manual/configuration/ad-hoc-network-config.section.md +++ b/nixos/doc/manual/configuration/ad-hoc-network-config.section.md @@ -1,14 +1,24 @@ # Ad-Hoc Configuration {#ad-hoc-network-config} -You can use [](#opt-networking.localCommands) to -specify shell commands to be run at the end of `network-setup.service`. This -is useful for doing network configuration not covered by the existing NixOS -modules. For instance, to statically configure an IPv6 address: +You can use [](#opt-networking.localCommands) to specify shell commands to be +run after the network interfaces have been created, but not necessarily fully +configured. +This is useful for doing network configuration not covered by the existing +NixOS modules. For example, you can create a network namespace and a pair +of virtual ethernet devices like this: ```nix { networking.localCommands = '' - ip -6 addr add 2001:610:685:1::1/64 dev eth0 + ip netns add mynet + ip link add name veth-in type veth peer name veth-out + ip link set dev veth-out netns mynet ''; } ``` + +::: {.note} +The commands should ideally be idempotent, so it's recommended to perform +cleanups of the state you create (e.g. virtual interfaces), or at least make +sure possible failures are handled. +::: diff --git a/nixos/doc/manual/configuration/ipv4-config.section.md b/nixos/doc/manual/configuration/ipv4-config.section.md index ed0db9da8c3e..ceb9a350b680 100644 --- a/nixos/doc/manual/configuration/ipv4-config.section.md +++ b/nixos/doc/manual/configuration/ipv4-config.section.md @@ -26,9 +26,16 @@ servers: ``` ::: {.note} -Statically configured interfaces are set up by the systemd service -`interface-name-cfg.service`. The default gateway and name server -configuration is performed by `network-setup.service`. +Addresses and routes for statically configured interfaces and the default +gateway are set up by systemd services named +`network-addresses-.service`. The name servers configuration, +instead, is performed by `network-local-commands.service` using resolvconf. +::: + +::: {.note} +If needed, for example if addresses/routes were added/removed, +you can reset the network configuration by running +`systemctl restart networking-scripted.target` ::: The host name is set using [](#opt-networking.hostName): diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 155611c02527..cda1c583018e 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -315,6 +315,13 @@ See . Note for NetworkManager users: before these changes NetworkManager used to spawn its own wpa_supplicant daemon, but now it relies on `networking.wireless`. So, if you had `networking.wireless.enable = false` in your configuration, you should remove that line. +- Some implementation details of the NixOS network-interfaces module have been changed: + + - In the "scripted" backend, `network-setup.service` has been removed and the network configuration services are now part of `network.target`, which is now directly pulled into `multi-user.target`. + - Interface addresses, routes and default gateways are now configured asynchronously as soon as the underlying network devices become available (fixes issue [#154737](https://github.com/NixOS/nixpkgs/issues/154737)). + - In both "networkd" and "scripted" backends, the configuration of name servers is now part of `network-local-commands.service` (fixes issue [#445496](https://github.com/NixOS/nixpkgs/issues/445496)). + - The issue that resulted in a completely unconfigured network if both `resolvconf` was disabled and no default gateway configured, has also been fixed. + - `kratos` has been updated from 1.3.1 to [25.4.0](https://github.com/ory/kratos/releases/tag/v25.4.0). Upstream switched to a new versioning scheme (year.major.minor). Notable breaking changes: - The `migrate sql` CLI command is now `migrate sql up` diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix index 74653918f893..5852b6340700 100644 --- a/nixos/modules/config/console.nix +++ b/nixos/modules/config/console.nix @@ -23,10 +23,12 @@ let ''; # Sadly, systemd-vconsole-setup doesn't support binary keymaps. - vconsoleConf = pkgs.writeText "vconsole.conf" '' - KEYMAP=${cfg.keyMap} - ${lib.optionalString (cfg.font != null) "FONT=${cfg.font}"} - ''; + vconsoleConf = + withFont: + pkgs.writeText "vconsole.conf" '' + KEYMAP=${cfg.keyMap} + ${lib.optionalString (withFont && cfg.font != null) "FONT=${cfg.font}"} + ''; consoleEnv = kbd: @@ -163,7 +165,7 @@ in # Let systemd-vconsole-setup.service do the work of setting up the # virtual consoles. - environment.etc."vconsole.conf".source = vconsoleConf; + environment.etc."vconsole.conf".source = vconsoleConf true; # Provide kbd with additional packages. environment.etc.kbd.source = "${consoleEnv pkgs.kbd}/share"; @@ -180,7 +182,7 @@ in ); boot.initrd.systemd.contents = { - "/etc/vconsole.conf".source = vconsoleConf; + "/etc/vconsole.conf".source = vconsoleConf cfg.earlySetup; # Add everything if we want full console setup... "/etc/kbd" = lib.mkIf cfg.earlySetup { source = "${consoleEnv config.boot.initrd.systemd.package.kbd}/share"; @@ -189,9 +191,6 @@ in "/etc/kbd/keymaps" = lib.mkIf (!cfg.earlySetup) { source = "${consoleEnv config.boot.initrd.systemd.package.kbd}/share/keymaps"; }; - "/etc/kbd/consolefonts" = lib.mkIf (!cfg.earlySetup && cfg.font != null) { - source = "${consoleEnv config.boot.initrd.systemd.package.kbd}/share/consolefonts"; - }; }; boot.initrd.systemd.additionalUpstreamUnits = [ "systemd-vconsole-setup.service" @@ -201,7 +200,7 @@ in "${config.boot.initrd.systemd.package.kbd}/bin/setfont" "${config.boot.initrd.systemd.package.kbd}/bin/loadkeys" ] - ++ lib.optionals (cfg.font != null && lib.hasPrefix builtins.storeDir cfg.font) [ + ++ lib.optionals (cfg.font != null && cfg.earlySetup && lib.hasPrefix builtins.storeDir cfg.font) [ "${cfg.font}" ] ++ lib.optionals (lib.hasPrefix builtins.storeDir cfg.keyMap) [ @@ -216,7 +215,7 @@ in description = "Reset console on configuration changes"; wantedBy = [ "multi-user.target" ]; restartTriggers = [ - vconsoleConf + (config.environment.etc."vconsole.conf".source) (consoleEnv pkgs.kbd) ]; reloadIfChanged = true; diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix index 086d8971d466..fb1f5612ad25 100644 --- a/nixos/modules/installer/tools/tools.nix +++ b/nixos/modules/installer/tools/tools.nix @@ -76,7 +76,7 @@ let { inputs = { # This is pointing to an unstable release. - # If you prefer a stable release instead, you can this to the latest number shown here: https://nixos.org/download + # If you prefer a stable release instead, you can change the word unstable to the latest number shown here: https://nixos.org/download # i.e. nixos-24.11 # Use `nix flake update` to update the flake to the latest revision of the chosen release channel. nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index de2168281c80..9c230e11960a 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -143,6 +143,8 @@ let "final.target" "kexec.target" "systemd-kexec.service" + "soft-reboot.target" + "systemd-soft-reboot.service" ] ++ lib.optional cfg.package.withUtmp "systemd-update-utmp.service" ++ [ diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix index 25c3ea7274a6..c294a3a6d436 100644 --- a/nixos/modules/system/etc/etc-activation.nix +++ b/nixos/modules/system/etc/etc-activation.nix @@ -1,7 +1,6 @@ { config, lib, - pkgs, ... }: @@ -51,6 +50,9 @@ ]; boot.initrd.systemd = { + storePaths = lib.mkIf config.system.etc.overlay.mutable [ + "${config.system.nixos-init.package}/bin/clear-etc-opaque" + ]; mounts = [ { where = "/run/nixos-etc-metadata"; @@ -131,13 +133,20 @@ before = [ "initrd-fs.target" ]; unitConfig = { DefaultDependencies = false; - RequiresMountsFor = "/sysroot"; + RequiresMountsFor = [ + "/sysroot" + # Needed so we can clear stale opaque markers from the + # upperdir based on the contents of the new metadata layer + # before the overlay is mounted. + "/run/nixos-etc-metadata" + ]; }; serviceConfig = { Type = "oneshot"; - ExecStart = '' - /bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work - ''; + ExecStart = [ + "/bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work" + "${config.system.nixos-init.package}/bin/clear-etc-opaque /run/nixos-etc-metadata /sysroot/.rw-etc/upper" + ]; }; }; }) diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix index 65d3a5192c45..60f69a7d3a07 100644 --- a/nixos/modules/system/etc/etc.nix +++ b/nixos/modules/system/etc/etc.nix @@ -285,6 +285,13 @@ in tmpMetadataMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc-metadata.XXXXXXXXXX) mount --type erofs --options ro,nodev,nosuid ${config.system.build.etcMetadataImage} "$tmpMetadataMount" + ${lib.optionalString config.system.etc.overlay.mutable '' + # Clear stale opaque markers from the upperdir so that lowerdir + # entries added by the new generation are not hidden. + # See https://github.com/NixOS/nixpkgs/issues/505475 + ${config.system.nixos-init.package}/bin/clear-etc-opaque "$tmpMetadataMount" /.rw-etc/upper + ''} + # There was no previous /etc mounted. This happens when we're called # directly without an initrd, like with nixos-enter. if ! mountpoint -q /etc; then diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix index 4b457e078e77..7961cb21ff5a 100644 --- a/nixos/modules/tasks/network-interfaces-scripted.nix +++ b/nixos/modules/tasks/network-interfaces-scripted.nix @@ -51,6 +51,71 @@ let (lib.concatStringsSep " ") ]; + # Converts an IPv4 address literal to a list of bits + parseAddr.ipv4 = + addr: + let + pad = b: lib.replicate (8 - builtins.length b) 0 ++ b; + toBin = n: pad (lib.toBaseDigits 2 (lib.toInt n)); + in + lib.concatMap toBin (builtins.splitVersion addr); + + # Converts an IPv6 address literal to a list of bits + parseAddr.ipv6 = + addr: + let + pad = b: lib.replicate (16 - builtins.length b) 0 ++ b; + fromHex = n: (builtins.fromTOML "n = 0x${n}").n; + toBin = n: pad (lib.toBaseDigits 2 (fromHex n)); + normal = (lib.network.ipv6.fromString addr).address; + in + lib.concatMap toBin (lib.splitString ":" normal); + + # Checks if `addr` is part of the `net` subnet + inSubnet = + v: net: addr: + let + prefix = lib.take net.prefixLength (parseAddr.${v} net.address); + match = lib.zipListsWith (a: b: a == b) prefix (parseAddr.${v} addr); + in + lib.all lib.id match; + + # Checks if the netmask of all addresses on interface `iface` includes + # the IP address of `gateway` + # + # Note: this is used to check whether networking.defaultGateway relies on + # the given interface, either explicitly, via the `interface` (optional), + # or explicitly, by using an address in a subnet of this interface. + # + # Configuration of the default gateway is then performed as part of that + # interface setup in `configureAddrs`, below. + isGateway = + v: gateway: iface: + lib.any lib.id ( + [ (iface.name == gateway.interface) ] + ++ map (net: inSubnet v net gateway.address) iface.${v}.addresses + ); + + # Checks if `gateway` uses an address from `iface` as default source + # + # Note: this is needed to delay the configuration of the gateway and default + # source until the right interfaces and address have been set up, otherwise + # the commands will fail. + hasSource = + v: gateway: iface: + builtins.elem gateway.source (map (i: i.address) iface.${v}.addresses); + + # Interfaces corresponding to the default gateways + gateway4Iface = builtins.filter (isGateway "ipv4" cfg.defaultGateway) interfaces; + gateway6Iface = builtins.filter (isGateway "ipv6" cfg.defaultGateway6) interfaces; + + # Interfaces corresponding to the default source addresses + # + # Note: the use of `head` here is safe because these expressions + # are evaluated only when `needsSourceIface`, see `configureAddrs` below. + source4Iface = builtins.head (builtins.filter (hasSource "ipv4" cfg.defaultGateway) interfaces); + source6Iface = builtins.head (builtins.filter (hasSource "ipv6" cfg.defaultGateway6) interfaces); + # warn that these attributes are deprecated (2017-2-2) # Should be removed in the release after next bondDeprecation = rec { @@ -118,121 +183,71 @@ let else optional (!config.boot.isContainer) (subsystemDevice dev); - hasDefaultGatewaySet = - (cfg.defaultGateway != null && cfg.defaultGateway.address != "") - || (cfg.enableIPv6 && cfg.defaultGateway6 != null && cfg.defaultGateway6.address != ""); - - needNetworkSetup = - cfg.resolvconf.enable || cfg.defaultGateway != null || cfg.defaultGateway6 != null; - - networkLocalCommands = lib.mkIf needNetworkSetup { - after = [ "network-setup.service" ]; - bindsTo = [ "network-setup.service" ]; - }; - - networkSetup = lib.mkIf needNetworkSetup { - description = "Networking Setup"; - - after = [ "network-pre.target" ]; - before = [ - "network.target" - "shutdown.target" - ]; - wants = [ "network.target" ]; - # exclude bridges from the partOf relationship to fix container networking bug #47210 - partOf = map (i: "network-addresses-${i.name}.service") ( - filter (i: !(hasAttr i.name cfg.bridges)) interfaces - ); - conflicts = [ "shutdown.target" ]; - wantedBy = [ "multi-user.target" ] ++ optional hasDefaultGatewaySet "network-online.target"; - - unitConfig.ConditionCapability = "CAP_NET_ADMIN"; - - path = [ pkgs.iproute2 ]; - - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - - unitConfig.DefaultDependencies = false; - - script = '' - ${optionalString config.networking.resolvconf.enable '' - # Set the static DNS configuration, if given. - ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <, create a job ‘network-addresses-.service" - # that performs static address configuration. It has a "wants" - # dependency on ‘.service’, which is supposed to create - # the interface and need not exist (i.e. for hardware - # interfaces). It has a binds-to dependency on the actual - # network device, so it only gets started after the interface - # has appeared, and it's stopped when the interface - # disappears. + # For each interface , creates a network-addresses-.service + # job that performs static address configuration. + # + # It has a Wants dependency on -netdev.service, which creates + # create the interface, or on a device unit (for hardware interfaces). + # It also has a BindsTo dependency on the device unit: so, it only gets + # started after the interface has appeared and it's stopped when the + # interface disappears. + # + # Unless in a container, the job is not made part of network.target, so + # if an interface is not found (e.g. a USB interface not plugged in) it + # will not hang the boot sequence. + # + # If the interface is the default gateway, the job will also set the + # default gateway and delay network-online.target. configureAddrs = i: let ips = interfaceIps i; + isDefaultGateway4 = cfg.defaultGateway != null && builtins.elem i gateway4Iface; + isDefaultGateway6 = cfg.defaultGateway6 != null && builtins.elem i gateway6Iface; + needsSourceIface4 = + isDefaultGateway4 && cfg.defaultGateway.source != null && i.name != source4Iface.name; + needsSourceIface6 = + isDefaultGateway6 && cfg.defaultGateway6.source != null && i.name != source6Iface.name; + + configureGateway = + version: gateway: + optionalString (gateway.address != "") '' + echo -n "setting ${i.name} as default IPv${version} gateway... " + ${optionalString (gateway.interface != null) '' + ip -${version} route replace ${gateway.address} proto static ${ + formatIpArgs { + metric = gateway.metric; + dev = gateway.interface; + } + } + ''} + ip -${version} route replace default proto static ${ + formatIpArgs { + metric = gateway.metric; + via = gateway.address; + window = cfg.defaultGatewayWindowSize; + dev = gateway.interface; + src = gateway.source; + } + } + echo "done" + ''; in nameValuePair "network-addresses-${i.name}" { description = "Address configuration of ${i.name}"; - wantedBy = [ - "network-setup.service" - "network.target" - ]; - # order before network-setup because the routes that are configured - # there may need ip addresses configured - before = [ "network-setup.service" ]; + + wantedBy = + deviceDependency i.name + ++ optional config.boot.isContainer "network.target" + ++ optional (isDefaultGateway4 || isDefaultGateway6) "network-online.target"; bindsTo = deviceDependency i.name; - after = [ "network-pre.target" ] ++ (deviceDependency i.name); + partOf = [ "networking-scripted.target" ]; + after = [ + "network-pre.target" + ] + ++ optional needsSourceIface4 "network-addresses-${source4Iface.name}.service" + ++ optional needsSourceIface6 "network-addresses-${source6Iface.name}.service" + ++ deviceDependency i.name; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; # Restart rather than stop+start this unit to prevent the @@ -284,6 +299,10 @@ let fi '' )} + + # Set the default gateway + ${optionalString isDefaultGateway4 (configureGateway "4" cfg.defaultGateway)} + ${optionalString isDefaultGateway6 (configureGateway "6" cfg.defaultGateway6)} ''; preStop = '' state="/run/nixos/network/routes/${i.name}" @@ -311,13 +330,13 @@ let nameValuePair "${i.name}-netdev" { description = "Virtual Network Interface ${i.name}"; bindsTo = optional (!config.boot.isContainer) "dev-net-tun.device"; + partOf = [ "networking-scripted.target" ]; after = optional (!config.boot.isContainer) "dev-net-tun.device" ++ [ "network-pre.target" ]; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice i.name) ]; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; path = [ pkgs.iproute2 ]; serviceConfig = { Type = "oneshot"; @@ -343,18 +362,21 @@ let description = "Bridge Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps ++ optional v.rstp "mstpd.service"; - partOf = [ "network-setup.service" ] ++ optional v.rstp "mstpd.service"; + partOf = [ + "network.target" + "networking-scripted.target" + ] + ++ optional v.rstp "mstpd.service"; after = [ "network-pre.target" ] ++ deps ++ optional v.rstp "mstpd.service" ++ map (i: "network-addresses-${i}.service") v.interfaces; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -448,15 +470,14 @@ let description = "Open vSwitch Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ] ++ internalConfigs; - # before = [ "network-setup.service" ]; - # should work without internalConfigs dependencies because address/link configuration depends - # on the device, which is created by ovs-vswitchd with type=internal, but it does not... - before = [ "network-setup.service" ] ++ internalConfigs; - partOf = [ "network-setup.service" ]; # shutdown the bridge when network is shutdown + before = [ "network.target" ] ++ internalConfigs; + partOf = [ + "network.target" + "networking-scripted.target" + ]; # shutdown the bridge when network is shutdown bindsTo = [ "ovs-vswitchd.service" ]; # requires ovs-vswitchd to be alive at all times after = [ "network-pre.target" @@ -521,12 +542,12 @@ let description = "Bond Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; + partOf = [ "networking-scripted.target" ]; after = [ "network-pre.target" ] ++ deps ++ map (i: "network-addresses-${i}.service") v.interfaces; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ @@ -570,12 +591,12 @@ let description = "MACVLAN Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; + partOf = [ "networking-scripted.target" ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -602,12 +623,12 @@ let description = "IPVLAN Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; + partOf = [ "networking-scripted.target" ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -647,12 +668,12 @@ let description = "FOU endpoint ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; + partOf = [ "networking-scripted.target" ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -677,12 +698,11 @@ let description = "IPv6 in IPv4 Tunnel Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -720,12 +740,12 @@ let description = "IP in IP Tunnel Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; + partOf = [ "networking-scripted.target" ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -768,12 +788,12 @@ let description = "GRE Tunnel Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; + partOf = [ "networking-scripted.target" ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -803,13 +823,15 @@ let description = "VLAN Interface ${n}"; wantedBy = [ "network.target" - "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; - partOf = [ "network-setup.service" ]; + partOf = [ + "network.target" + "networking-scripted.target" + ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network-setup.service" ]; + before = [ "network.target" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -845,13 +867,39 @@ let // mapAttrs' createGreDevice cfg.greTunnels // mapAttrs' createVlanDevice cfg.vlans // { - network-setup = networkSetup; - network-local-commands = networkLocalCommands; + network-local-commands = { + after = [ "network-pre.target" ]; + wantedBy = [ "network.target" ]; + }; }; - services.udev.extraRules = '' - KERNEL=="tun", TAG+="systemd" - ''; + # Note: the scripted networking backend consistent of many + # independent services that are linked to the network.target. + # Since there is no daemon (e.g systemd-networkd) that is + # started as part of the system and pulls in network.target. + # Thus, to start these services we link network.target directly + # to multi-user.target, this has the same result. + systemd.targets.network.wantedBy = [ "multi-user.target" ]; + + # This target serves no purpose during the boot, but can be + # used to quickly reset the network configuration by running + # systemctl restart networking-scripted.target + systemd.targets.networking-scripted = { + description = "NixOS scripted networking setup"; + }; + + services.udev.extraRules = lib.concatStringsSep "\n" ( + [ ''KERNEL=="tun", TAG+="systemd"'' ] + # This creates a udev rule to start each service with a WantedBy + # dependency on a device unit. It's needed because if the service + # unit is loaded in stage 2 but its device was already up by + # stage 1, systemd will not automatically start it. + ++ lib.forEach (lib.attrNames cfg.interfaces) ( + iface: + ''ACTION=="add", SUBSYSTEM=="net", KERNEL=="${iface}", '' + + ''ENV{SYSTEMD_WANTS}="network-addresses-${iface}.service"'' + ) + ); }; diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index 2b01843b82dc..4887256b7be1 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -747,10 +747,9 @@ in default = ""; example = "text=anything; echo You can put $text here."; description = '' - Shell commands to be executed at the end of the - `network-setup` systemd service. Note that if - you are using DHCP to obtain the network configuration, - interfaces may not be fully configured yet. + Shell commands to be executed after all the network + interfaces have been created, but not necessarily + fully configured. ''; }; @@ -1853,6 +1852,20 @@ in ''; }; }; + + networking.localCommands = lib.mkIf config.networking.resolvconf.enable '' + # Set the static DNS configuration, if given. + ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <&1 || true")) + with subtest("switching to the same generation"): machine.succeed("/run/current-system/bin/switch-to-configuration test") @@ -77,6 +90,15 @@ assert machine.succeed("cat /etc/newgen") == "newgen" assert machine.succeed("cat /etc/mutable") == "mutable" + # Regression test for https://github.com/NixOS/nixpkgs/issues/505475: + # The opaque /etc/nixos in the upperdir (created by stage-2-init.sh + # before /nixos existed in the lowerdir) must not hide lowerdir entries + # added by the new generation. The activation script must have cleared + # the stale opaque marker. + print(machine.succeed("ls -la /etc/nixos/")) + machine.succeed("test -L /etc/nixos/newlink") + machine.fail("getfattr -h -n trusted.overlay.opaque /.rw-etc/upper/nixos") + print(machine.succeed("findmnt /etc/mountpoint")) print(machine.succeed("stat /etc/mountpoint/extra-file")) print(machine.succeed("findmnt /etc/filemount")) @@ -93,5 +115,23 @@ numOfMetaMounts = len(metaMounts.splitlines()) assert numOfTmpMounts == 0, f"Found {numOfTmpMounts} remaining tmpmounts" assert numOfMetaMounts == 1, f"Found {numOfMetaMounts} remaining metamounts" + + with subtest("stale opaque markers are cleared by initrd on boot (NixOS/nixpkgs#505475)"): + # Simulate the bug precondition: an opaque /pam.d in the upperdir. + # /pam.d is guaranteed to exist as a directory in the metadata layer. + machine.succeed("mkdir -p /.rw-etc/upper/pam.d") + machine.succeed("setfattr -h -n trusted.overlay.opaque -v y /.rw-etc/upper/pam.d") + machine.succeed("getfattr -h -n trusted.overlay.opaque /.rw-etc/upper/pam.d") + # Also create a non-opaque upperdir directory that exists in the + # metadata layer, to ensure clear-etc-opaque tolerates the + # already-clear case. + machine.succeed("mkdir -p /.rw-etc/upper/systemd") + + # Reboot and verify the initrd rw-etc service cleared the opaque marker. + machine.shutdown() + machine.start() + machine.wait_for_unit("multi-user.target") + machine.fail("getfattr -h -n trusted.overlay.opaque /.rw-etc/upper/pam.d") + machine.succeed("test -e /etc/pam.d/login") ''; } diff --git a/nixos/tests/networking/networkd-and-scripted.nix b/nixos/tests/networking/networkd-and-scripted.nix index 99e67649ceb3..d620e9b230fb 100644 --- a/nixos/tests/networking/networkd-and-scripted.nix +++ b/nixos/tests/networking/networkd-and-scripted.nix @@ -44,17 +44,13 @@ let defaultGateway6 = { address = "fd00:1234:5678:1::1"; interface = "enp1s0"; - source = "fd00:1234:5678:1::3"; + source = "fd00:1234:5678:1::3"; # implicit dependency on enp2s0 }; interfaces.enp1s0.ipv6.addresses = [ { address = "fd00:1234:5678:1::2"; prefixLength = 64; } - { - address = "fd00:1234:5678:1::3"; - prefixLength = 128; - } ]; interfaces.enp1s0.ipv4.addresses = [ { @@ -76,6 +72,12 @@ let prefixLength = 24; } ]; + interfaces.enp2s0.ipv6.addresses = [ + { + address = "fd00:1234:5678:1::3"; + prefixLength = 128; + } + ]; }; }; testScript = '' @@ -108,6 +110,41 @@ let client.succeed("ip -6 route show default | grep -q 'src fd00:1234:5678:1::3'") ''; }; + dynamicInterface = { + name = "dynamicInterface"; + nodes.machine = clientConfig { + networking.interfaces.usb0 = { + ipv6.addresses = lib.singleton { + address = "fd::1"; + prefixLength = 127; + }; + }; + networking.defaultGateway6 = { + address = "fd::"; + interface = "usb0"; + source = "fd::1"; + }; + }; + testScript = '' + with subtest("Network comes up without usb0"): + machine.wait_for_unit("network.target") + + with subtest("multi-user.target does not hang"): + machine.require_unit_state("multi-user.target", "active") + + with subtest("usb0 is configured when plugged in"): + machine.succeed("ip link add usb0 type sit local 1.2.3.4") + machine.wait_until_succeeds("ip addr show dev usb0 | grep -q fd::1") + + with subtest("Network is now online"): + machine.systemctl("start network-online.target") + machine.require_unit_state("network-online.target", "active") + + with subtest("Default gateway is now set"): + machine.succeed("ip -6 route show default | grep -q 'via fd::'") + machine.succeed("ip -6 route show default | grep -q 'src fd::1'") + ''; + }; routeType = { name = "RouteType"; nodes.client = clientConfig { diff --git a/pkgs/by-name/ni/nixos-init/Cargo.lock b/pkgs/by-name/ni/nixos-init/Cargo.lock index 5aee8eedf701..e9d0c98cb115 100644 --- a/pkgs/by-name/ni/nixos-init/Cargo.lock +++ b/pkgs/by-name/ni/nixos-init/Cargo.lock @@ -142,6 +142,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "xattr", ] [[package]] @@ -506,3 +507,13 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] diff --git a/pkgs/by-name/ni/nixos-init/Cargo.toml b/pkgs/by-name/ni/nixos-init/Cargo.toml index 46102be28304..6955ab5f091b 100644 --- a/pkgs/by-name/ni/nixos-init/Cargo.toml +++ b/pkgs/by-name/ni/nixos-init/Cargo.toml @@ -11,6 +11,7 @@ pathrs = "0.2.2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" bootspec = "2.0.0" +xattr = "1.6.1" [dev-dependencies] tempfile = "3.20.0" diff --git a/pkgs/by-name/ni/nixos-init/README.md b/pkgs/by-name/ni/nixos-init/README.md index 4d1677db0015..96b50f3449ce 100644 --- a/pkgs/by-name/ni/nixos-init/README.md +++ b/pkgs/by-name/ni/nixos-init/README.md @@ -52,6 +52,9 @@ closure. Currently nixos-init comes in at ~500 KiB. - `find-etc`: Finds the `/etc` paths in `/sysroot` so that the initrd doesn't directly depend on the toplevel, reducing the need to rebuild the initrd on every generation. +- `clear-etc-opaque`: Clears stale `trusted.overlay.opaque` xattrs from the + mutable `/etc` overlay's upperdir before it is mounted, so that lowerdir + entries added by a new generation are not hidden. - `resolve-in-root`: Figures out the canonical path inside a chroot. ## Future diff --git a/pkgs/by-name/ni/nixos-init/package.nix b/pkgs/by-name/ni/nixos-init/package.nix index c84c711c7599..85d394e10485 100644 --- a/pkgs/by-name/ni/nixos-init/package.nix +++ b/pkgs/by-name/ni/nixos-init/package.nix @@ -47,6 +47,7 @@ rustPlatform.buildRustPackage (finalAttrs: { binaries = [ "initrd-init" "find-etc" + "clear-etc-opaque" "resolve-in-root" "env-generator" ]; diff --git a/pkgs/by-name/ni/nixos-init/src/etc_overlay.rs b/pkgs/by-name/ni/nixos-init/src/etc_overlay.rs new file mode 100644 index 000000000000..a5c681ff0869 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/etc_overlay.rs @@ -0,0 +1,170 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result, bail}; + +const OVERLAY_OPAQUE_XATTR: &str = "trusted.overlay.opaque"; + +/// Entrypoint for the `clear-etc-opaque` binary. +/// +/// When a directory is created in the mutable `/etc` overlay that does not yet +/// exist in the lowerdir, overlayfs marks it opaque in the upperdir. This is +/// correct at creation time, but becomes stale when a later generation adds +/// entries under that same directory to the metadata layer: the opaque marker +/// hides them. +/// +/// This walks the (newly mounted) metadata layer and removes +/// `trusted.overlay.opaque` from any upperdir directory that now has a +/// directory counterpart in the lowerdir, turning it back into a merged view. +/// Files the user placed in the upperdir remain visible (upperdir wins +/// per-entry) and individual whiteouts are preserved; only the blanket hiding +/// of lowerdir content is undone. +/// +/// See . +/// +/// Usage: `clear-etc-opaque ` +pub fn clear_etc_opaque() -> Result<()> { + let args: Vec = env::args().collect(); + + if args.len() != 3 { + bail!("Usage: {} ", args[0]); + } + + let metadata_mount = PathBuf::from(&args[1]); + let upperdir = PathBuf::from(&args[2]); + + if !upperdir.is_dir() { + // Nothing to clear (e.g. first boot before the upperdir is created). + log::info!( + "Upperdir {} does not exist, nothing to clear.", + upperdir.display() + ); + return Ok(()); + } + + clear_opaque_markers(&metadata_mount, &metadata_mount, &upperdir) +} + +/// Recursively walk `current` (a subtree of `metadata_root`) and clear the +/// opaque xattr from the corresponding directory in `upperdir`. +fn clear_opaque_markers(metadata_root: &Path, current: &Path, upperdir: &Path) -> Result<()> { + let entries = fs::read_dir(current) + .with_context(|| format!("Failed to read directory {}", current.display()))?; + + for entry in entries { + let entry = + entry.with_context(|| format!("Failed to read entry in {}", current.display()))?; + + // Use the entry's own type info (no symlink following) so we only + // recurse into real directories of the metadata image. + if !entry + .file_type() + .with_context(|| format!("Failed to stat {}", entry.path().display()))? + .is_dir() + { + continue; + } + + let path = entry.path(); + let rel = path + .strip_prefix(metadata_root) + .context("Failed to strip metadata root prefix")?; + let target = upperdir.join(rel); + + // Only act on real directories in the upperdir; an opaque marker on a + // non-directory would be meaningless and we must not follow symlinks + // out of the upperdir. + match fs::symlink_metadata(&target) { + Ok(meta) if meta.is_dir() => { + remove_opaque_xattr(&target); + // Only recurse when the upperdir also has this directory: + // deeper lowerdir directories without an upperdir counterpart + // cannot carry stale markers. + clear_opaque_markers(metadata_root, &path, upperdir)?; + } + // Missing or not a directory: nothing to do for this subtree. + _ => {} + } + } + + Ok(()) +} + +/// Remove the `trusted.overlay.opaque` xattr from `path` if present. +fn remove_opaque_xattr(path: &Path) { + // Check first instead of removing unconditionally: lremovexattr(2) reports + // a missing attribute as ENODATA, which std does not map to a stable + // io::ErrorKind, so distinguishing it from real errors is awkward. + match xattr::get(path, OVERLAY_OPAQUE_XATTR) { + Ok(None) => return, + Ok(Some(_)) => {} + Err(err) => { + log::warn!( + "Failed to read {OVERLAY_OPAQUE_XATTR} on {}: {err}.", + path.display() + ); + return; + } + } + + match xattr::remove(path, OVERLAY_OPAQUE_XATTR) { + Ok(()) => { + log::info!("Cleared stale opaque marker from {}.", path.display()); + } + Err(err) => { + // Don't abort the boot over this; the worst case is that some + // declaratively-managed /etc entries stay hidden, which is what + // would happen anyway without this fixup. + log::warn!( + "Failed to remove {OVERLAY_OPAQUE_XATTR} from {}: {err}.", + path.display() + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use tempfile::tempdir; + + #[test] + fn clears_opaque_only_for_matching_dirs() -> Result<()> { + if !xattr::SUPPORTED_PLATFORM { + return Ok(()); + } + + let metadata = tempdir()?; + let upper = tempdir()?; + + // lowerdir gained nixos/sub in the new generation. + fs::create_dir_all(metadata.path().join("nixos/sub"))?; + // upperdir has an opaque nixos/ from before. + fs::create_dir_all(upper.path().join("nixos"))?; + // upperdir directory without a lowerdir counterpart: we only clear + // markers where the lowerdir has a matching directory, so this one + // must stay opaque. + fs::create_dir_all(upper.path().join("only-upper"))?; + + // The build sandbox usually lacks CAP_SYS_ADMIN, so trusted.* xattrs + // cannot be set. Skip in that case rather than fail the build. + if xattr::set(upper.path().join("nixos"), OVERLAY_OPAQUE_XATTR, b"y").is_err() { + eprintln!("skipping: cannot set trusted.* xattrs in this environment"); + return Ok(()); + } + xattr::set(upper.path().join("only-upper"), OVERLAY_OPAQUE_XATTR, b"y")?; + + clear_opaque_markers(metadata.path(), metadata.path(), upper.path())?; + + assert!(xattr::get(upper.path().join("nixos"), OVERLAY_OPAQUE_XATTR)?.is_none()); + assert_eq!( + xattr::get(upper.path().join("only-upper"), OVERLAY_OPAQUE_XATTR)?.as_deref(), + Some(b"y".as_slice()) + ); + + Ok(()) + } +} diff --git a/pkgs/by-name/ni/nixos-init/src/lib.rs b/pkgs/by-name/ni/nixos-init/src/lib.rs index e9a716724051..c781757c9b82 100644 --- a/pkgs/by-name/ni/nixos-init/src/lib.rs +++ b/pkgs/by-name/ni/nixos-init/src/lib.rs @@ -1,6 +1,7 @@ mod activate; mod config; mod env_generator; +mod etc_overlay; mod find_etc; mod fs; mod init; @@ -16,6 +17,7 @@ use anyhow::{Context, Result, bail}; pub use crate::{ activate::activate, env_generator::env_generator, + etc_overlay::clear_etc_opaque, find_etc::find_etc, init::init, initrd_init::initrd_init, diff --git a/pkgs/by-name/ni/nixos-init/src/main.rs b/pkgs/by-name/ni/nixos-init/src/main.rs index 4f74415b8d36..bef10017f1ea 100644 --- a/pkgs/by-name/ni/nixos-init/src/main.rs +++ b/pkgs/by-name/ni/nixos-init/src/main.rs @@ -2,7 +2,7 @@ use std::{env, io::Write, process::ExitCode}; use log::Level; -use nixos_init::{env_generator, find_etc, initrd_init, resolve_in_root}; +use nixos_init::{clear_etc_opaque, env_generator, find_etc, initrd_init, resolve_in_root}; fn main() -> ExitCode { let arg0 = env::args() @@ -12,6 +12,7 @@ fn main() -> ExitCode { setup_logger(); let entrypoint = match arg0.as_str() { + "clear-etc-opaque" => clear_etc_opaque, "find-etc" => find_etc, "resolve-in-root" => resolve_in_root, "initrd-init" => initrd_init, diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index cc7186fb0960..401229afad54 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -25,18 +25,18 @@ "lts": true }, "6.12": { - "version": "6.12.87", - "hash": "sha256:0c4qidff0qs2x0mvba83cw3ksaz2af3xwabvc839xvsc9djaf4nc", + "version": "6.12.88", + "hash": "sha256:1s2x66j46gxw17j8hh5iws0l00aivmphp3mn8vgwbn8sj44gacjb", "lts": true }, "6.18": { - "version": "6.18.29", - "hash": "sha256:0g584ak8p9nqxysn8d1qzxbp1asfd39hqy9np35gri9v3xdsfgn3", + "version": "6.18.30", + "hash": "sha256:1m5kvzky4g2jc8b09np8can0lasg1lwjfgdj41s3ymxgqdp8icx8", "lts": true }, "7.0": { - "version": "7.0.6", - "hash": "sha256:08vm18wx6399phzgr3wz94yga3ab4fyca79445ygvbspm904996b", + "version": "7.0.7", + "hash": "sha256:1x2xnb7gpj0inxdc317zi71i0d98b7wq64s0yzk2vzxalf3gxqf8", "lts": false } } diff --git a/pkgs/tools/package-management/nix/modular/src/nix/package.nix b/pkgs/tools/package-management/nix/modular/src/nix/package.nix index deb49c9e7b4d..737b57f9433e 100644 --- a/pkgs/tools/package-management/nix/modular/src/nix/package.nix +++ b/pkgs/tools/package-management/nix/modular/src/nix/package.nix @@ -1,6 +1,7 @@ { lib, mkMesonExecutable, + stdenv, nix-store, nix-expr, @@ -12,6 +13,11 @@ # Configuration Options version, + + # Whether to link against mimalloc for malloc override. + # Significantly improves evaluation performance on allocation-heavy + # workloads (~10-15% on large evaluations). + withMimalloc ? !stdenv.hostPlatform.isWindows, }: mkMesonExecutable (finalAttrs: { @@ -25,10 +31,11 @@ mkMesonExecutable (finalAttrs: { nix-expr nix-main nix-cmd - mimalloc - ]; + ] + ++ lib.optional ((lib.versionAtLeast version "2.35pre") && withMimalloc) mimalloc; - mesonFlags = [ + mesonFlags = lib.optionals (lib.versionAtLeast version "2.35pre") [ + (lib.mesonEnable "mimalloc" withMimalloc) ]; meta = {