From b6f7d1c851e90f540e938db89e934b8b1c5e4ffe Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 8 Apr 2026 13:41:08 +0200 Subject: [PATCH 1/5] nixos/tmpfiles: fix layer inversion The nix module uses tmpfiles, not the other way around. Move definitions so that tmpfiles does not depend on the nix module. This makes the tmpfiles module usable without loading a nix module. --- nixos/modules/services/system/nix-daemon.nix | 169 ++++++++++-------- .../modules/system/boot/systemd/tmpfiles.nix | 8 - 2 files changed, 90 insertions(+), 87 deletions(-) diff --git a/nixos/modules/services/system/nix-daemon.nix b/nixos/modules/services/system/nix-daemon.nix index bceaf82798fc..97a5752bd35c 100644 --- a/nixos/modules/services/system/nix-daemon.nix +++ b/nixos/modules/services/system/nix-daemon.nix @@ -297,94 +297,105 @@ in ###### implementation - config = lib.mkIf (cfg.enable && nixPackage.pname != "lix") { - environment.systemPackages = [ - nixPackage - pkgs.nix-info - ] - ++ lib.optional (config.programs.bash.completion.enable) pkgs.nix-bash-completions; - - systemd.packages = [ nixPackage ]; - - # The upstream Nix tmpfiles.d file assumes the daemon runs as root - systemd.tmpfiles.packages = lib.mkIf (cfg.daemonUser == "root") [ nixPackage ]; - - systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ]; - - systemd.services.nix-daemon = { - path = [ + config = lib.mkMerge [ + (lib.mkIf cfg.enable { + systemd.tmpfiles.rules = [ + "d /nix/var 0755 root root - -" + "L+ /nix/var/nix/gcroots/booted-system 0755 root root - /run/booted-system" + # Boot-time cleanup + "R! /nix/var/nix/gcroots/tmp - - - - -" + "R! /nix/var/nix/temproots - - - - -" + ]; + }) + (lib.mkIf (cfg.enable && nixPackage.pname != "lix") { + environment.systemPackages = [ nixPackage - config.programs.ssh.package + pkgs.nix-info ] - # For running "newuidmap" - ++ lib.optional (cfg.daemonUser != "root") "/run/wrappers"; + ++ lib.optional (config.programs.bash.completion.enable) pkgs.nix-bash-completions; - environment = - cfg.envVars - // { - CURL_CA_BUNDLE = config.security.pki.caBundle; - } - // config.networking.proxy.envVars; + systemd.packages = [ nixPackage ]; + + # The upstream Nix tmpfiles.d file assumes the daemon runs as root + systemd.tmpfiles.packages = lib.mkIf (cfg.daemonUser == "root") [ nixPackage ]; + + systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ]; + + systemd.services.nix-daemon = { + path = [ + nixPackage + config.programs.ssh.package + ] + # For running "newuidmap" + ++ lib.optional (cfg.daemonUser != "root") "/run/wrappers"; + + environment = + cfg.envVars + // { + CURL_CA_BUNDLE = config.security.pki.caBundle; + } + // config.networking.proxy.envVars; + + serviceConfig = { + CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy; + IOSchedulingClass = cfg.daemonIOSchedClass; + IOSchedulingPriority = cfg.daemonIOSchedPriority; + }; + + restartTriggers = [ config.environment.etc."nix/nix.conf".source ]; + + # `stopIfChanged = false` changes to switch behavior + # from stop -> update units -> start + # to update units -> restart + # + # The `stopIfChanged` setting therefore controls a trade-off between a + # more predictable lifecycle, which runs the correct "version" of + # the `ExecStop` line, and on the other hand the availability of + # sockets during the switch, as the effectiveness of the stop operation + # depends on the socket being stopped as well. + # + # As `nix-daemon.service` does not make use of `ExecStop`, we prefer + # to keep the socket up and available. This is important for machines + # that run Nix-based services, such as automated build, test, and deploy + # services, that expect the daemon socket to be available at all times. + # + # Notably, the Nix client does not retry on failure to connect to the + # daemon socket, and the in-process RemoteStore instance will disable + # itself. This makes retries infeasible even for services that are + # aware of the issue. Failure to connect can affect not only new client + # processes, but also new RemoteStore instances in existing processes, + # as well as existing RemoteStore instances that have not saturated + # their connection pool. + # + # Also note that `stopIfChanged = true` does not kill existing + # connection handling daemons, as one might wish to happen before a + # breaking Nix upgrade (which is rare). The daemon forks that handle + # the individual connections split off into their own sessions, causing + # them not to be stopped by systemd. + # If a Nix upgrade does require all existing daemon processes to stop, + # nix-daemon must do so on its own accord, and only when the new version + # starts and detects that Nix's persistent state needs an upgrade. + stopIfChanged = false; - serviceConfig = { - CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy; - IOSchedulingClass = cfg.daemonIOSchedClass; - IOSchedulingPriority = cfg.daemonIOSchedPriority; }; - restartTriggers = [ config.environment.etc."nix/nix.conf".source ]; + # Set up the environment variables for running Nix. + environment.sessionVariables = cfg.envVars; - # `stopIfChanged = false` changes to switch behavior - # from stop -> update units -> start - # to update units -> restart - # - # The `stopIfChanged` setting therefore controls a trade-off between a - # more predictable lifecycle, which runs the correct "version" of - # the `ExecStop` line, and on the other hand the availability of - # sockets during the switch, as the effectiveness of the stop operation - # depends on the socket being stopped as well. - # - # As `nix-daemon.service` does not make use of `ExecStop`, we prefer - # to keep the socket up and available. This is important for machines - # that run Nix-based services, such as automated build, test, and deploy - # services, that expect the daemon socket to be available at all times. - # - # Notably, the Nix client does not retry on failure to connect to the - # daemon socket, and the in-process RemoteStore instance will disable - # itself. This makes retries infeasible even for services that are - # aware of the issue. Failure to connect can affect not only new client - # processes, but also new RemoteStore instances in existing processes, - # as well as existing RemoteStore instances that have not saturated - # their connection pool. - # - # Also note that `stopIfChanged = true` does not kill existing - # connection handling daemons, as one might wish to happen before a - # breaking Nix upgrade (which is rare). The daemon forks that handle - # the individual connections split off into their own sessions, causing - # them not to be stopped by systemd. - # If a Nix upgrade does require all existing daemon processes to stop, - # nix-daemon must do so on its own accord, and only when the new version - # starts and detects that Nix's persistent state needs an upgrade. - stopIfChanged = false; + nix.nrBuildUsers = lib.mkDefault ( + if cfg.settings.auto-allocate-uids or false then + 0 + else + lib.max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs) + ); - }; + users.users = nixbldUsers; - # Set up the environment variables for running Nix. - environment.sessionVariables = cfg.envVars; + services.displayManager.hiddenUsers = lib.attrNames nixbldUsers; - nix.nrBuildUsers = lib.mkDefault ( - if cfg.settings.auto-allocate-uids or false then - 0 - else - lib.max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs) - ); - - users.users = nixbldUsers; - - services.displayManager.hiddenUsers = lib.attrNames nixbldUsers; - - # Legacy configuration conversion. - nix.settings.sandbox-fallback = false; - }; + # Legacy configuration conversion. + nix.settings.sandbox-fallback = false; + }) + ]; } diff --git a/nixos/modules/system/boot/systemd/tmpfiles.nix b/nixos/modules/system/boot/systemd/tmpfiles.nix index 12eca48f945d..c606ff1ac50b 100644 --- a/nixos/modules/system/boot/systemd/tmpfiles.nix +++ b/nixos/modules/system/boot/systemd/tmpfiles.nix @@ -350,19 +350,11 @@ in "d /var/db 0755 root root - -" "L /var/lock - - - - ../run/lock" ] - ++ lib.optionals config.nix.enable [ - "d /nix/var 0755 root root - -" - "L+ /nix/var/nix/gcroots/booted-system 0755 root root - /run/booted-system" - ] # Boot-time cleanup ++ [ "R! /etc/group.lock - - - - -" "R! /etc/passwd.lock - - - - -" "R! /etc/shadow.lock - - - - -" - ] - ++ lib.optionals config.nix.enable [ - "R! /nix/var/nix/gcroots/tmp - - - - -" - "R! /nix/var/nix/temproots - - - - -" ]; boot.initrd.systemd = { From 692e94a1cf24eeab2129935c63a5094611389b1b Mon Sep 17 00:00:00 2001 From: whispers Date: Fri, 10 Apr 2026 18:00:56 -0400 Subject: [PATCH 2/5] nixos-rebuild-ng: don't resolve /run/current-system symlink for --diff Before running `nix store diff-closures`, `nixos-rebuild-ng` tried to read the symlink on the local system. While this provides for better logging, when paired with `--target-host`, it causes deploy failures as it tries to diff the config of the local system against the target. Even if the store path of the local system happens to be on the target, the diff is incorrect, as its comparing two different systems against each other instead of the old and new closures on the target system. Accordingly, we simply avoid resolving the symlink on the host system. If a future solution would like to fix this, it should make sure to do the reading for debug printing on the *target* sytem, not the local one. --- pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py index 8a170a138326..4de74aa9c4d4 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py @@ -323,7 +323,7 @@ def build_and_activate_system( if args.diff: if current_config.exists(): nix.diff_closures( - current_config=current_config.readlink(), + current_config=current_config, new_config=path_to_config, target_host=target_host, ) From d0ea24542dac2ad37ae9644544a105af4c1a17ea Mon Sep 17 00:00:00 2001 From: Majiir Paktu Date: Fri, 20 Feb 2026 19:18:11 -0500 Subject: [PATCH 3/5] nixos/pam: rename u2fAuth -> u2f.enable --- nixos/modules/programs/i3lock.nix | 4 ++-- nixos/modules/security/pam.nix | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/nixos/modules/programs/i3lock.nix b/nixos/modules/programs/i3lock.nix index 1b4091abaac3..79b62635a454 100644 --- a/nixos/modules/programs/i3lock.nix +++ b/nixos/modules/programs/i3lock.nix @@ -32,7 +32,7 @@ in description = '' Whether to enable U2F support in the i3lock program. U2F enables authentication using a hardware device, such as a security key. - When U2F support is enabled, the i3lock program will set the setuid bit on the i3lock binary and enable the pam u2fAuth service, + When U2F support is enabled, the i3lock program will set the setuid bit on the i3lock binary and enable the pam u2f service, ''; }; }; @@ -51,7 +51,7 @@ in source = "${cfg.package.out}/bin/i3lock"; }; - security.pam.services.i3lock.u2fAuth = cfg.u2fSupport; + security.pam.services.i3lock.u2f.enable = cfg.u2fSupport; }; diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index d568b32a45a3..7ec024b5e09c 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -135,6 +135,7 @@ let imports = [ (lib.mkRenamedOptionModule [ "enableKwallet" ] [ "kwallet" "enable" ]) + (lib.mkRenamedOptionModule [ "u2fAuth" ] [ "u2f" "enable" ]) ]; options = { @@ -202,17 +203,19 @@ let ''; }; - u2fAuth = lib.mkOption { - default = config.security.pam.u2f.enable; - defaultText = lib.literalExpression "config.security.pam.u2f.enable"; - type = lib.types.bool; - description = '' - If set, users listed in - {file}`$XDG_CONFIG_HOME/Yubico/u2f_keys` (or - {file}`$HOME/.config/Yubico/u2f_keys` if XDG variable is - not set) are able to log in with the associated U2F key. Path can be - changed using {option}`security.pam.u2f.authFile` option. - ''; + u2f = { + enable = lib.mkOption { + default = config.security.pam.u2f.enable; + defaultText = lib.literalExpression "config.security.pam.u2f.enable"; + type = lib.types.bool; + description = '' + If set, users listed in + {file}`$XDG_CONFIG_HOME/Yubico/u2f_keys` (or + {file}`$HOME/.config/Yubico/u2f_keys` if XDG variable is + not set) are able to log in with the associated U2F key. Path can be + changed using {option}`security.pam.u2f.authFile` option. + ''; + }; }; usshAuth = lib.mkOption { @@ -1045,7 +1048,7 @@ let in { name = "u2f"; - enable = cfg.u2fAuth; + enable = cfg.u2f.enable; control = u2f.control; modulePath = "${pkgs.pam_u2f}/lib/security/pam_u2f.so"; inherit (u2f) settings; From 2fb68fdb50f795be3e01fdf8d793e1669fbf8e21 Mon Sep 17 00:00:00 2001 From: Majiir Paktu Date: Fri, 20 Feb 2026 19:27:00 -0500 Subject: [PATCH 4/5] nixos/pam: add u2f.control option --- nixos/modules/security/pam.nix | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 7ec024b5e09c..539029e16978 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -216,6 +216,26 @@ let changed using {option}`security.pam.u2f.authFile` option. ''; }; + + control = lib.mkOption { + default = config.security.pam.u2f.control; + defaultText = lib.literalExpression "config.security.pam.u2f.control"; + type = lib.types.enum [ + "required" + "requisite" + "sufficient" + "optional" + ]; + description = '' + This option sets pam "control". + If you want to have multi factor authentication, use "required". + If you want to use U2F device instead of regular password, use "sufficient". + + Read + {manpage}`pam.conf(5)` + for better understanding of this option. + ''; + }; }; usshAuth = lib.mkOption { @@ -1049,7 +1069,7 @@ let { name = "u2f"; enable = cfg.u2f.enable; - control = u2f.control; + control = cfg.u2f.control; modulePath = "${pkgs.pam_u2f}/lib/security/pam_u2f.so"; inherit (u2f) settings; } From 85a509502df8222c6116a333a68f6748d80bb9ea Mon Sep 17 00:00:00 2001 From: Will Fancher Date: Sun, 12 Apr 2026 16:46:07 -0400 Subject: [PATCH 5/5] Revert #481473 "nixos/network-interfaces: remove network-setup" Reverts #481473, fixes #509254 Turns out this breaks scripted networking with systemd stage 1. Even if we assumed that systemd stage 1 wasn't now the default, I still think that makes this worthy of a revert, in the absence of a fix. But now that systemd stage 1 is the default, this is also a channel blocker. The problem is that wantedBy = [ "foo.device" ]; doesn't work as hoped. If the device appears in stage 1, then the systemd device unit becomes active in stage 1 without pulling in any dependency for configuring it (because that only exists in stage 2). When we transition to stage 2, systemd maintains its information about unit state between stages and remembers that the device units were already active. They do not become reactivated, and consequently their new stage 2 dependencies do not get pulled in. The fix is probably to do this with SYSTEMD_WANTS= in udev rules, rather than with wantedBy. The udev rules will re-fire during stage 2, though I'm not 100% clear on exactly which events that includes (e.g. if we'll get new add events or if we need to look for a different ACTION or anything like that). This is going to require some experimentation and testing that I don't have time for today. Given that this is addressing a channel blocker, I think this revert should be merged if a fix can't be merged instead before the next unstable-small eval. In either case, staging-nixos should be merged to master right after. --- .../ad-hoc-network-config.section.md | 20 +- .../configuration/ipv4-config.section.md | 13 +- .../manual/release-notes/rl-2605.section.md | 7 - .../tasks/network-interfaces-scripted.nix | 313 ++++++++---------- nixos/modules/tasks/network-interfaces.nix | 21 +- .../networking/networkd-and-scripted.nix | 47 +-- 6 files changed, 154 insertions(+), 267 deletions(-) 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 611f3e3956ee..7ef24825526d 100644 --- a/nixos/doc/manual/configuration/ad-hoc-network-config.section.md +++ b/nixos/doc/manual/configuration/ad-hoc-network-config.section.md @@ -1,24 +1,14 @@ # Ad-Hoc Configuration {#ad-hoc-network-config} -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: +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: ```nix { networking.localCommands = '' - ip netns add mynet - ip link add name veth-in type veth peer name veth-out - ip link set dev veth-out netns mynet + ip -6 addr add 2001:610:685:1::1/64 dev eth0 ''; } ``` - -::: {.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 ceb9a350b680..ed0db9da8c3e 100644 --- a/nixos/doc/manual/configuration/ipv4-config.section.md +++ b/nixos/doc/manual/configuration/ipv4-config.section.md @@ -26,16 +26,9 @@ servers: ``` ::: {.note} -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` +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`. ::: 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 80320c9bc670..09a82d0bce35 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -261,13 +261,6 @@ 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/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix index 4bf274d4157f..4b457e078e77 100644 --- a/nixos/modules/tasks/network-interfaces-scripted.nix +++ b/nixos/modules/tasks/network-interfaces-scripted.nix @@ -51,71 +51,6 @@ 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 { @@ -183,71 +118,121 @@ let else optional (!config.boot.isContainer) (subsystemDevice dev); - # 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. + 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. 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 = - deviceDependency i.name - ++ optional config.boot.isContainer "network.target" - ++ optional (isDefaultGateway4 || isDefaultGateway6) "network-online.target"; + 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" ]; bindsTo = 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; + after = [ "network-pre.target" ] ++ (deviceDependency i.name); serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; # Restart rather than stop+start this unit to prevent the @@ -299,10 +284,6 @@ 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}" @@ -330,13 +311,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.target" ]; + before = [ "network-setup.service" ]; path = [ pkgs.iproute2 ]; serviceConfig = { Type = "oneshot"; @@ -362,21 +343,18 @@ let description = "Bridge Interface ${n}"; wantedBy = [ "network.target" + "network-setup.service" (subsystemDevice n) ]; bindsTo = deps ++ optional v.rstp "mstpd.service"; - partOf = [ - "network.target" - "networking-scripted.target" - ] - ++ optional v.rstp "mstpd.service"; + partOf = [ "network-setup.service" ] ++ 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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -470,14 +448,15 @@ let description = "Open vSwitch Interface ${n}"; wantedBy = [ "network.target" + "network-setup.service" (subsystemDevice n) ] ++ internalConfigs; - before = [ "network.target" ] ++ internalConfigs; - partOf = [ - "network.target" - "networking-scripted.target" - ]; # shutdown the bridge when network is shutdown + # 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 bindsTo = [ "ovs-vswitchd.service" ]; # requires ovs-vswitchd to be alive at all times after = [ "network-pre.target" @@ -542,12 +521,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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ @@ -591,12 +570,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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -623,12 +602,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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -668,12 +647,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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -698,11 +677,12 @@ 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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -740,12 +720,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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -788,12 +768,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.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -823,15 +803,13 @@ let description = "VLAN Interface ${n}"; wantedBy = [ "network.target" + "network-setup.service" (subsystemDevice n) ]; bindsTo = deps; - partOf = [ - "network.target" - "networking-scripted.target" - ]; + partOf = [ "network-setup.service" ]; after = [ "network-pre.target" ] ++ deps; - before = [ "network.target" ]; + before = [ "network-setup.service" ]; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; path = [ pkgs.iproute2 ]; @@ -867,27 +845,10 @@ let // mapAttrs' createGreDevice cfg.greTunnels // mapAttrs' createVlanDevice cfg.vlans // { - network-local-commands = { - after = [ "network-pre.target" ]; - wantedBy = [ "network.target" ]; - }; + network-setup = networkSetup; + network-local-commands = networkLocalCommands; }; - # 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 = '' KERNEL=="tun", TAG+="systemd" ''; diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index d0fe0ed8443f..83d60ba80e40 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -747,9 +747,10 @@ in default = ""; example = "text=anything; echo You can put $text here."; description = '' - Shell commands to be executed after all the network - interfaces have been created, but not necessarily - fully configured. + 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. ''; }; @@ -1850,20 +1851,6 @@ in ''; }; }; - - networking.localCommands = lib.mkIf config.networking.resolvconf.enable '' - # Set the static DNS configuration, if given. - ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <