Merge pull request #169116 from ElvishJerricco/systemd-stage-1-networkd

Systemd stage 1 networkd
This commit is contained in:
Florian Klink 2023-04-21 18:40:59 +02:00 committed by GitHub
commit 6b27ed3229
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 715 additions and 224 deletions

View file

@ -428,6 +428,8 @@ let
uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid";
sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid";
spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
inherit (cfg) mutableUsers;
@ -534,6 +536,54 @@ in {
WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
'';
};
# systemd initrd
boot.initrd.systemd.users = mkOption {
visible = false;
description = ''
Users to include in initrd.
'';
default = {};
type = types.attrsOf (types.submodule ({ name, ... }: {
options.uid = mkOption {
visible = false;
type = types.int;
description = ''
ID of the user in initrd.
'';
defaultText = literalExpression "config.users.users.\${name}.uid";
default = cfg.users.${name}.uid;
};
options.group = mkOption {
visible = false;
type = types.singleLineStr;
description = ''
Group the user belongs to in initrd.
'';
defaultText = literalExpression "config.users.users.\${name}.group";
default = cfg.users.${name}.group;
};
}));
};
boot.initrd.systemd.groups = mkOption {
visible = false;
description = ''
Groups to include in initrd.
'';
default = {};
type = types.attrsOf (types.submodule ({ name, ... }: {
options.gid = mkOption {
visible = false;
type = types.int;
description = ''
ID of the group in initrd.
'';
defaultText = literalExpression "config.users.groups.\${name}.gid";
default = cfg.groups.${name}.gid;
};
}));
};
};
@ -639,10 +689,52 @@ in {
"/etc/profiles/per-user/$USER"
];
# systemd initrd
boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
contents = {
"/etc/passwd".text = ''
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group }: let
g = config.boot.initrd.systemd.groups.${group};
in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:") config.boot.initrd.systemd.users)}
'';
"/etc/group".text = ''
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)}
'';
};
users = {
root = {};
nobody = {};
};
groups = {
root = {};
nogroup = {};
systemd-journal = {};
tty = {};
dialout = {};
kmem = {};
input = {};
video = {};
render = {};
sgx = {};
audio = {};
video = {};
lp = {};
disk = {};
cdrom = {};
tape = {};
kvm = {};
};
};
assertions = [
{ assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
message = "UIDs and GIDs must be unique!";
}
{ assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
message = "systemd initrd UIDs and GIDs must be unique!";
}
{ # If mutableUsers is false, to prevent users creating a
# configuration that locks them out of the system, ensure that
# there is at least one "privileged" account that has a

View file

@ -16,16 +16,6 @@ let
'';
# networkd link files are used early by udev to set up interfaces early.
# This must be done in stage 1 to avoid race conditions between udev and
# network daemons.
# TODO move this into the initrd-network module when it exists
initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} ''
mkdir -p $out
ln -s ${udev}/lib/systemd/network/*.link $out/
${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))}
'';
extraUdevRules = pkgs.writeTextFile {
name = "extra-udev-rules";
text = cfg.extraRules;
@ -398,7 +388,6 @@ in
systemd = config.boot.initrd.systemd.package;
binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
};
"/etc/systemd/network".source = initrdLinkUnits;
};
# Insert initrd rules
boot.initrd.services.udev.packages = [

View file

@ -14,13 +14,17 @@ let
serviceDirectories = cfg.packages;
};
inherit (lib) mkOption mkIf mkMerge types;
inherit (lib) mkOption mkEnableOption mkIf mkMerge types;
in
{
options = {
boot.initrd.systemd.dbus = {
enable = mkEnableOption (lib.mdDoc "dbus in stage 1") // { visible = false; };
};
services.dbus = {
enable = mkOption {
@ -111,6 +115,21 @@ in
];
}
(mkIf config.boot.initrd.systemd.dbus.enable {
boot.initrd.systemd = {
users.messagebus = { };
groups.messagebus = { };
contents."/etc/dbus-1".source = pkgs.makeDBusConf {
inherit (cfg) apparmor;
suidHelper = "/bin/false";
serviceDirectories = [ pkgs.dbus ];
};
packages = [ pkgs.dbus ];
storePaths = [ "${pkgs.dbus}/bin/dbus-daemon" ];
targets.sockets.wants = [ "dbus.socket" ];
};
})
(mkIf (cfg.implementation == "dbus") {
environment.systemPackages = [
pkgs.dbus

View file

@ -67,11 +67,15 @@ in
boot.initrd.network.flushBeforeStage2 = mkOption {
type = types.bool;
default = true;
default = !config.boot.initrd.systemd.enable;
defaultText = "!config.boot.initrd.systemd.enable";
description = lib.mdDoc ''
Whether to clear the configuration of the interfaces that were set up in
the initrd right before stage 2 takes over. Stage 2 will do the regular network
configuration based on the NixOS networking options.
The default is false when systemd is enabled in initrd,
because the systemd-networkd documentation suggests it.
'';
};

View file

@ -51,7 +51,7 @@ in
# Add openvpn and ip binaries to the initrd
# The shared libraries are required for DNS resolution
boot.initrd.extraUtilsCommands = ''
boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
copy_bin_and_libs ${pkgs.iproute2}/bin/ip
@ -59,18 +59,33 @@ in
cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
'';
boot.initrd.systemd.storePaths = [
"${pkgs.openvpn}/bin/openvpn"
"${pkgs.iproute2}/bin/ip"
"${pkgs.glibc}/lib/libresolv.so.2"
"${pkgs.glibc}/lib/libnss_dns.so.2"
];
boot.initrd.secrets = {
"/etc/initrd.ovpn" = cfg.configuration;
};
# openvpn --version would exit with 1 instead of 0
boot.initrd.extraUtilsCommandsTest = ''
boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
$out/bin/openvpn --show-gateway
'';
boot.initrd.network.postCommands = ''
boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
openvpn /etc/initrd.ovpn &
'';
boot.initrd.systemd.services.openvpn = {
wantedBy = [ "initrd.target" ];
path = [ pkgs.iproute2 ];
after = [ "network.target" "initrd-nixos-copy-secrets.service" ];
serviceConfig.ExecStart = "${pkgs.openvpn}/bin/openvpn /etc/initrd.ovpn";
serviceConfig.Type = "notify";
};
};
}

View file

@ -5,6 +5,10 @@ with lib;
let
cfg = config.boot.initrd.network.ssh;
shell = if cfg.shell == null then "/bin/ash" else cfg.shell;
inherit (config.programs.ssh) package;
enabled = let initrd = config.boot.initrd; in (initrd.network.enable || initrd.systemd.network.enable) && cfg.enable;
in
@ -33,8 +37,9 @@ in
};
shell = mkOption {
type = types.str;
default = "/bin/ash";
type = types.nullOr types.str;
default = null;
defaultText = ''"/bin/ash"'';
description = lib.mdDoc ''
Login shell of the remote user. Can be used to limit actions user can do.
'';
@ -119,9 +124,11 @@ in
sshdCfg = config.services.openssh;
sshdConfig = ''
UsePAM no
Port ${toString cfg.port}
PasswordAuthentication no
AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u
ChallengeResponseAuthentication no
${flip concatMapStrings cfg.hostKeys (path: ''
@ -142,7 +149,7 @@ in
${cfg.extraConfig}
'';
in mkIf (config.boot.initrd.network.enable && cfg.enable) {
in mkIf enabled {
assertions = [
{
assertion = cfg.authorizedKeys != [];
@ -157,14 +164,19 @@ in
for instructions.
'';
}
{
assertion = config.boot.initrd.systemd.enable -> cfg.shell == null;
message = "systemd stage 1 does not support boot.initrd.network.ssh.shell";
}
];
boot.initrd.extraUtilsCommands = ''
copy_bin_and_libs ${pkgs.openssh}/bin/sshd
boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
copy_bin_and_libs ${package}/bin/sshd
cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
'';
boot.initrd.extraUtilsCommandsTest = ''
boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
# sshd requires a host key to check config, so we pass in the test's
tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
@ -176,9 +188,9 @@ in
rm "$tmpkey"
'';
boot.initrd.network.postCommands = ''
echo '${cfg.shell}' > /etc/shells
echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd
boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
echo '${shell}' > /etc/shells
echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd
echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
echo 'passwd: files' > /etc/nsswitch.conf
@ -204,7 +216,7 @@ in
/bin/sshd -e
'';
boot.initrd.postMountCommands = ''
boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
# Stop sshd cleanly before stage 2.
#
# If you want to keep it around to debug post-mount SSH issues,
@ -217,6 +229,38 @@ in
boot.initrd.secrets = listToAttrs
(map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys);
# Systemd initrd stuff
boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable {
users.sshd = { uid = 1; group = "sshd"; };
groups.sshd = { gid = 1; };
contents."/etc/ssh/authorized_keys.d/root".text =
concatStringsSep "\n" config.boot.initrd.network.ssh.authorizedKeys;
contents."/etc/ssh/sshd_config".text = sshdConfig;
storePaths = ["${package}/bin/sshd"];
services.sshd = {
description = "SSH Daemon";
wantedBy = ["initrd.target"];
after = ["network.target" "initrd-nixos-copy-secrets.service"];
# Keys from Nix store are world-readable, which sshd doesn't
# like. If this were a real nix store and not the initrd, we
# neither would nor could do this
preStart = flip concatMapStrings cfg.hostKeys (path: ''
/bin/chmod 0600 "${initrdKeyPath path}"
'');
unitConfig.DefaultDependencies = false;
serviceConfig = {
ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config";
Type = "simple";
KillMode = "process";
Restart = "on-failure";
};
};
};
};
}

View file

@ -6,8 +6,6 @@ with lib;
let
cfg = config.systemd.network;
check = {
global = {
@ -2941,14 +2939,12 @@ let
+ def.extraConfig;
};
unitFiles = listToAttrs (map (name: {
name = "systemd/network/${name}";
mkUnitFiles = prefix: cfg: listToAttrs (map (name: {
name = "${prefix}systemd/network/${name}";
value.source = "${cfg.units.${name}.unit}/${name}";
}) (attrNames cfg.units));
in
{
options = {
commonOptions = {
systemd.network.enable = mkOption {
default = false;
@ -3051,12 +3047,11 @@ in
};
config = mkMerge [
commonConfig = config: let cfg = config.systemd.network; in mkMerge [
# .link units are honored by udev, no matter if systemd-networkd is enabled or not.
{
systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.link" (linkToUnit n v)) cfg.links;
environment.etc = unitFiles;
systemd.network.wait-online.extraArgs =
[ "--timeout=${toString cfg.wait-online.timeout}" ]
@ -3066,14 +3061,6 @@ in
(mkIf config.systemd.network.enable {
users.users.systemd-network.group = "systemd-network";
systemd.additionalUpstreamSystemUnits = [
"systemd-networkd-wait-online.service"
"systemd-networkd.service"
"systemd-networkd.socket"
];
systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.netdevs
// mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.networks;
@ -3082,14 +3069,6 @@ in
# networkd.
systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ];
systemd.services.systemd-networkd = {
wantedBy = [ "multi-user.target" ];
aliases = [ "dbus-org.freedesktop.network1.service" ];
restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
config.environment.etc."systemd/networkd.conf".source
];
};
systemd.services.systemd-networkd-wait-online = {
inherit (cfg.wait-online) enable;
wantedBy = [ "network-online.target" ];
@ -3111,8 +3090,37 @@ in
};
};
})
];
stage2Config = let
cfg = config.systemd.network;
unitFiles = mkUnitFiles "" cfg;
in mkMerge [
(commonConfig config)
{ environment.etc = unitFiles; }
(mkIf config.systemd.network.enable {
users.users.systemd-network.group = "systemd-network";
systemd.additionalUpstreamSystemUnits = [
"systemd-networkd-wait-online.service"
"systemd-networkd.service"
"systemd-networkd.socket"
];
environment.etc."systemd/networkd.conf" = renderConfig cfg.config;
systemd.services.systemd-networkd = {
wantedBy = [ "multi-user.target" ];
restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
config.environment.etc."systemd/networkd.conf".source
];
aliases = [ "dbus-org.freedesktop.network1.service" ];
};
networking.iproute2 = mkIf (cfg.config.addRouteTablesToIPRoute2 && cfg.config.routeTables != { }) {
enable = mkDefault true;
rttablesExtraConfig = ''
@ -3123,6 +3131,116 @@ in
};
services.resolved.enable = mkDefault true;
})
];
stage1Config = let
cfg = config.boot.initrd.systemd.network;
in mkMerge [
(commonConfig config.boot.initrd)
{
systemd.network.enable = mkDefault config.boot.initrd.network.enable;
systemd.contents = mkUnitFiles "/etc/" cfg;
# Networkd link files are used early by udev to set up interfaces early.
# This must be done in stage 1 to avoid race conditions between udev and
# network daemons.
systemd.network.units = lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units;
systemd.storePaths = ["${config.boot.initrd.systemd.package}/lib/systemd/network/99-default.link"];
}
(mkIf cfg.enable {
systemd.package = pkgs.systemdStage1Network;
# For networkctl
systemd.dbus.enable = mkDefault true;
systemd.additionalUpstreamUnits = [
"systemd-networkd-wait-online.service"
"systemd-networkd.service"
"systemd-networkd.socket"
"systemd-network-generator.service"
"network-online.target"
"network-pre.target"
"network.target"
"nss-lookup.target"
"nss-user-lookup.target"
"remote-fs-pre.target"
"remote-fs.target"
];
systemd.users.systemd-network = {};
systemd.groups.systemd-network = {};
systemd.contents."/etc/systemd/networkd.conf" = renderConfig cfg.config;
systemd.services.systemd-networkd.wantedBy = [ "initrd.target" ];
systemd.services.systemd-network-generator.wantedBy = [ "sysinit.target" ];
systemd.storePaths = [
"${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd"
"${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd-wait-online"
"${config.boot.initrd.systemd.package}/lib/systemd/systemd-network-generator"
];
kernelModules = [ "af_packet" ];
systemd.services.nixos-flush-networkd = mkIf config.boot.initrd.network.flushBeforeStage2 {
description = "Flush Network Configuration";
wantedBy = ["initrd.target"];
after = ["systemd-networkd.service" "dbus.socket" "dbus.service"];
before = ["shutdown.target" "initrd-switch-root.target"];
conflicts = ["shutdown.target" "initrd-switch-root.target"];
unitConfig.DefaultDependencies = false;
serviceConfig = {
# This service does nothing when starting, but brings down
# interfaces when switching root. This is the easiest way to
# ensure proper ordering while stopping. See systemd.unit(5)
# section on Before= and After=. The important part is that
# we are stopped before units we need, like dbus.service,
# and that we are stopped before starting units like
# initrd-switch-root.target
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/true";
};
# systemd-networkd doesn't bring down interfaces on its own
# when it exits (see: systemd-networkd(8)), so we have to do
# it ourselves. The networkctl command doesn't have a way to
# bring all interfaces down, so we have to iterate over the
# list and filter out unmanaged interfaces to bring them down
# individually.
preStop = ''
networkctl list --full --no-legend | while read _idx link _type _operational setup _; do
[ "$setup" = unmanaged ] && continue
networkctl down "$link"
done
'';
};
})
];
in
{
options = commonOptions // {
boot.initrd = commonOptions;
};
config = mkMerge [
stage2Config
(mkIf config.boot.initrd.systemd.enable {
assertions = [{
assertion = config.boot.initrd.network.udhcpc.extraArgs == [];
message = ''
boot.initrd.network.udhcpc.extraArgs is not supported when
boot.initrd.systemd.enable is enabled
'';
}];
boot.initrd = stage1Config;
})
];
}

View file

@ -445,7 +445,8 @@ let
) config.boot.initrd.secrets)
}
(cd "$tmp" && find . -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
# mindepth 1 so that we don't change the mode of /
(cd "$tmp" && find . -mindepth 1 -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
'';

View file

@ -19,13 +19,13 @@
# drop this service, we'd mount the /run tmpfs over the secret, making it
# invisible in stage 2.
script = ''
for secret in $(cd /.initrd-secrets; find . -type f); do
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
mkdir -p "$(dirname "/$secret")"
cp "/.initrd-secrets/$secret" "/$secret"
done
'';
unitConfig = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};

View file

@ -72,15 +72,6 @@ let
"systemd-tmpfiles-setup.service"
"timers.target"
"umount.target"
# TODO: Networking
# "network-online.target"
# "network-pre.target"
# "network.target"
# "nss-lookup.target"
# "nss-user-lookup.target"
# "remote-fs-pre.target"
# "remote-fs.target"
] ++ cfg.additionalUpstreamUnits;
upstreamWants = [
@ -378,7 +369,7 @@ in {
"/etc/systemd/system.conf".text = ''
[Manager]
DefaultEnvironment=PATH=/bin:/sbin ${optionalString (isBool cfg.emergencyAccess && cfg.emergencyAccess) "SYSTEMD_SULOGIN_FORCE=1"}
DefaultEnvironment=PATH=/bin:/sbin
${cfg.extraConfig}
ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
'';
@ -388,8 +379,10 @@ in {
"/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules;
"/etc/passwd".source = "${pkgs.fakeNss}/etc/passwd";
"/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then "!" else cfg.emergencyAccess}:::::::";
# We can use either ! or * to lock the root account in the
# console, but some software like OpenSSH won't even allow you
# to log in with an SSH key if you use ! so we use * instead
"/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then optionalString (!cfg.emergencyAccess) "*" else cfg.emergencyAccess}:::::::";
"/bin".source = "${initrdBinEnv}/bin";
"/sbin".source = "${initrdBinEnv}/sbin";

View file

@ -28,11 +28,164 @@ let
# TODO: warn the user that any address configured on those interfaces will be useless
++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);
domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
genericNetwork = override:
let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
makeGateway = gateway: {
routeConfig = {
Gateway = gateway;
GatewayOnLink = false;
};
};
in optionalAttrs (gateway != [ ]) {
routes = override (map makeGateway gateway);
} // optionalAttrs (domains != [ ]) {
domains = override domains;
};
genericDhcpNetworks = initrd: mkIf cfg.useDHCP {
networks."99-ethernet-default-dhcp" = {
# We want to match physical ethernet interfaces as commonly
# found on laptops, desktops and servers, to provide an
# "out-of-the-box" setup that works for common cases. This
# heuristic isn't perfect (it could match interfaces with
# custom names that _happen_ to start with en or eth), but
# should be good enough to make the common case easy and can
# be overridden on a case-by-case basis using
# higher-priority networks or by disabling useDHCP.
# Type=ether matches veth interfaces as well, and this is
# more likely to result in interfaces being configured to
# use DHCP when they shouldn't.
# When wait-online.anyInterface is enabled, RequiredForOnline really
# means "sufficient for online", so we can enable it.
# Otherwise, don't block the network coming online because of default networks.
matchConfig.Name = ["en*" "eth*"];
DHCP = "yes";
linkConfig.RequiredForOnline =
lib.mkDefault (if initrd
then config.boot.initrd.systemd.network.wait-online.anyInterface
else config.systemd.network.wait-online.anyInterface);
networkConfig.IPv6PrivacyExtensions = "kernel";
};
networks."99-wireless-client-dhcp" = {
# Like above, but this is much more likely to be correct.
matchConfig.WLANInterfaceType = "station";
DHCP = "yes";
linkConfig.RequiredForOnline =
lib.mkDefault config.systemd.network.wait-online.anyInterface;
networkConfig.IPv6PrivacyExtensions = "kernel";
# We also set the route metric to one more than the default
# of 1024, so that Ethernet is preferred if both are
# available.
dhcpV4Config.RouteMetric = 1025;
ipv6AcceptRAConfig.RouteMetric = 1025;
};
};
interfaceNetworks = mkMerge (forEach interfaces (i: {
netdevs = mkIf i.virtual ({
"40-${i.name}" = {
netdevConfig = {
Name = i.name;
Kind = i.virtualType;
};
"${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
User = i.virtualOwner;
};
};
});
networks."40-${i.name}" = mkMerge [ (genericNetwork id) {
name = mkDefault i.name;
DHCP = mkForce (dhcpStr
(if i.useDHCP != null then i.useDHCP else false));
address = forEach (interfaceIps i)
(ip: "${ip.address}/${toString ip.prefixLength}");
routes = forEach (interfaceRoutes i)
(route: {
# Most of these route options have not been tested.
# Please fix or report any mistakes you may find.
routeConfig =
optionalAttrs (route.address != null && route.prefixLength != null) {
Destination = "${route.address}/${toString route.prefixLength}";
} //
optionalAttrs (route.options ? fastopen_no_cookie) {
FastOpenNoCookie = route.options.fastopen_no_cookie;
} //
optionalAttrs (route.via != null) {
Gateway = route.via;
} //
optionalAttrs (route.type != null) {
Type = route.type;
} //
optionalAttrs (route.options ? onlink) {
GatewayOnLink = true;
} //
optionalAttrs (route.options ? initrwnd) {
InitialAdvertisedReceiveWindow = route.options.initrwnd;
} //
optionalAttrs (route.options ? initcwnd) {
InitialCongestionWindow = route.options.initcwnd;
} //
optionalAttrs (route.options ? pref) {
IPv6Preference = route.options.pref;
} //
optionalAttrs (route.options ? mtu) {
MTUBytes = route.options.mtu;
} //
optionalAttrs (route.options ? metric) {
Metric = route.options.metric;
} //
optionalAttrs (route.options ? src) {
PreferredSource = route.options.src;
} //
optionalAttrs (route.options ? protocol) {
Protocol = route.options.protocol;
} //
optionalAttrs (route.options ? quickack) {
QuickAck = route.options.quickack;
} //
optionalAttrs (route.options ? scope) {
Scope = route.options.scope;
} //
optionalAttrs (route.options ? from) {
Source = route.options.from;
} //
optionalAttrs (route.options ? table) {
Table = route.options.table;
} //
optionalAttrs (route.options ? advmss) {
TCPAdvertisedMaximumSegmentSize = route.options.advmss;
} //
optionalAttrs (route.options ? ttl-propagate) {
TTLPropagate = route.options.ttl-propagate == "enabled";
};
});
networkConfig.IPv6PrivacyExtensions = "kernel";
linkConfig = optionalAttrs (i.macAddress != null) {
MACAddress = i.macAddress;
} // optionalAttrs (i.mtu != null) {
MTUBytes = toString i.mtu;
};
}];
}));
in
{
config = mkMerge [
config = mkIf cfg.useNetworkd {
(mkIf config.boot.initrd.network.enable {
# Note this is if initrd.network.enable, not if
# initrd.systemd.network.enable. By setting the latter and not the
# former, the user retains full control over the configuration.
boot.initrd.systemd.network = mkMerge [(genericDhcpNetworks true) interfaceNetworks];
})
(mkIf cfg.useNetworkd {
assertions = [ {
assertion = cfg.defaultGatewayWindowSize == null;
@ -54,149 +207,11 @@ in
networking.dhcpcd.enable = mkDefault false;
systemd.network =
let
domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
genericNetwork = override:
let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
makeGateway = gateway: {
routeConfig = {
Gateway = gateway;
GatewayOnLink = false;
};
};
in optionalAttrs (gateway != [ ]) {
routes = override (map makeGateway gateway);
} // optionalAttrs (domains != [ ]) {
domains = override domains;
};
in mkMerge [ {
mkMerge [ {
enable = true;
}
(mkIf cfg.useDHCP {
networks."99-ethernet-default-dhcp" = lib.mkIf cfg.useDHCP {
# We want to match physical ethernet interfaces as commonly
# found on laptops, desktops and servers, to provide an
# "out-of-the-box" setup that works for common cases. This
# heuristic isn't perfect (it could match interfaces with
# custom names that _happen_ to start with en or eth), but
# should be good enough to make the common case easy and can
# be overridden on a case-by-case basis using
# higher-priority networks or by disabling useDHCP.
# Type=ether matches veth interfaces as well, and this is
# more likely to result in interfaces being configured to
# use DHCP when they shouldn't.
# When wait-online.anyInterface is enabled, RequiredForOnline really
# means "sufficient for online", so we can enable it.
# Otherwise, don't block the network coming online because of default networks.
matchConfig.Name = ["en*" "eth*"];
DHCP = "yes";
linkConfig.RequiredForOnline =
lib.mkDefault config.systemd.network.wait-online.anyInterface;
networkConfig.IPv6PrivacyExtensions = "kernel";
};
networks."99-wireless-client-dhcp" = lib.mkIf cfg.useDHCP {
# Like above, but this is much more likely to be correct.
matchConfig.WLANInterfaceType = "station";
DHCP = "yes";
linkConfig.RequiredForOnline =
lib.mkDefault config.systemd.network.wait-online.anyInterface;
networkConfig.IPv6PrivacyExtensions = "kernel";
# We also set the route metric to one more than the default
# of 1024, so that Ethernet is preferred if both are
# available.
dhcpV4Config.RouteMetric = 1025;
ipv6AcceptRAConfig.RouteMetric = 1025;
};
})
(mkMerge (forEach interfaces (i: {
netdevs = mkIf i.virtual ({
"40-${i.name}" = {
netdevConfig = {
Name = i.name;
Kind = i.virtualType;
};
"${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
User = i.virtualOwner;
};
};
});
networks."40-${i.name}" = mkMerge [ (genericNetwork id) {
name = mkDefault i.name;
DHCP = mkForce (dhcpStr
(if i.useDHCP != null then i.useDHCP else false));
address = forEach (interfaceIps i)
(ip: "${ip.address}/${toString ip.prefixLength}");
routes = forEach (interfaceRoutes i)
(route: {
# Most of these route options have not been tested.
# Please fix or report any mistakes you may find.
routeConfig =
optionalAttrs (route.address != null && route.prefixLength != null) {
Destination = "${route.address}/${toString route.prefixLength}";
} //
optionalAttrs (route.options ? fastopen_no_cookie) {
FastOpenNoCookie = route.options.fastopen_no_cookie;
} //
optionalAttrs (route.via != null) {
Gateway = route.via;
} //
optionalAttrs (route.type != null) {
Type = route.type;
} //
optionalAttrs (route.options ? onlink) {
GatewayOnLink = true;
} //
optionalAttrs (route.options ? initrwnd) {
InitialAdvertisedReceiveWindow = route.options.initrwnd;
} //
optionalAttrs (route.options ? initcwnd) {
InitialCongestionWindow = route.options.initcwnd;
} //
optionalAttrs (route.options ? pref) {
IPv6Preference = route.options.pref;
} //
optionalAttrs (route.options ? mtu) {
MTUBytes = route.options.mtu;
} //
optionalAttrs (route.options ? metric) {
Metric = route.options.metric;
} //
optionalAttrs (route.options ? src) {
PreferredSource = route.options.src;
} //
optionalAttrs (route.options ? protocol) {
Protocol = route.options.protocol;
} //
optionalAttrs (route.options ? quickack) {
QuickAck = route.options.quickack;
} //
optionalAttrs (route.options ? scope) {
Scope = route.options.scope;
} //
optionalAttrs (route.options ? from) {
Source = route.options.from;
} //
optionalAttrs (route.options ? table) {
Table = route.options.table;
} //
optionalAttrs (route.options ? advmss) {
TCPAdvertisedMaximumSegmentSize = route.options.advmss;
} //
optionalAttrs (route.options ? ttl-propagate) {
TTLPropagate = route.options.ttl-propagate == "enabled";
};
});
networkConfig.IPv6PrivacyExtensions = "kernel";
linkConfig = optionalAttrs (i.macAddress != null) {
MACAddress = i.macAddress;
} // optionalAttrs (i.mtu != null) {
MTUBytes = toString i.mtu;
};
}];
})))
(genericDhcpNetworks false)
interfaceNetworks
(mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
netdevs."40-${name}" = {
netdevConfig = {
@ -437,6 +452,7 @@ in
bindsTo = [ "systemd-networkd.service" ];
};
};
};
})
];
}

View file

@ -869,6 +869,8 @@ in
boot.initrd.kernelModules = optionals (cfg.useNixStoreImage && !cfg.writableStore) [ "erofs" ];
boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false);
boot.initrd.extraUtilsCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable)
''
# We need mke2fs in the initrd.

View file

@ -680,6 +680,9 @@ in {
systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {};
systemd-initrd-swraid = handleTest ./systemd-initrd-swraid.nix {};
systemd-initrd-vconsole = handleTest ./systemd-initrd-vconsole.nix {};
systemd-initrd-networkd = handleTest ./systemd-initrd-networkd.nix {};
systemd-initrd-networkd-ssh = handleTest ./systemd-initrd-networkd-ssh.nix {};
systemd-initrd-networkd-openvpn = handleTest ./initrd-network-openvpn { systemdStage1 = true; };
systemd-journal = handleTest ./systemd-journal.nix {};
systemd-machinectl = handleTest ./systemd-machinectl.nix {};
systemd-networkd = handleTest ./systemd-networkd.nix {};

View file

@ -1,3 +1,9 @@
{ system ? builtins.currentSystem
, config ? {}
, pkgs ? import ../.. { inherit system config; }
, systemdStage1 ? false
}:
import ../make-test-python.nix ({ lib, ...}:
{
@ -22,11 +28,12 @@ import ../make-test-python.nix ({ lib, ...}:
minimalboot =
{ ... }:
{
boot.initrd.systemd.enable = systemdStage1;
boot.initrd.network = {
enable = true;
openvpn = {
enable = true;
configuration = "/dev/null";
configuration = builtins.toFile "initrd.ovpn" "";
};
};
};
@ -39,6 +46,17 @@ import ../make-test-python.nix ({ lib, ...}:
virtualisation.vlans = [ 1 ];
boot.initrd = {
systemd.enable = systemdStage1;
systemd.extraBin.nc = "${pkgs.busybox}/bin/nc";
systemd.services.nc = {
requiredBy = ["initrd.target"];
after = ["network.target"];
serviceConfig = {
ExecStart = "/bin/nc -p 1234 -lke /bin/echo TESTVALUE";
Type = "oneshot";
};
};
# This command does not fork to keep the VM in the state where
# only the initramfs is loaded
preLVMCommands =

View file

@ -22,10 +22,6 @@ import ../make-test-python.nix ({ lib, ... }:
hostKeys = [ ./ssh_host_ed25519_key ];
};
};
boot.initrd.extraUtilsCommands = ''
mkdir -p $out/secrets/etc/ssh
cat "${./ssh_host_ed25519_key}" > $out/secrets/etc/ssh/sh_host_ed25519_key
'';
boot.initrd.preLVMCommands = ''
while true; do
if [ -f fnord ]; then

View file

@ -8,25 +8,48 @@ let
testCombinations = pkgs.lib.cartesianProductOfSets {
predictable = [true false];
withNetworkd = [true false];
systemdStage1 = [true false];
};
in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd }: {
in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd, systemdStage1 }: {
name = pkgs.lib.optionalString (!predictable) "un" + "predictable"
+ pkgs.lib.optionalString withNetworkd "Networkd";
+ pkgs.lib.optionalString withNetworkd "Networkd"
+ pkgs.lib.optionalString systemdStage1 "SystemdStage1";
value = makeTest {
name = "${pkgs.lib.optionalString (!predictable) "un"}predictableInterfaceNames${pkgs.lib.optionalString withNetworkd "-with-networkd"}";
name = pkgs.lib.optionalString (!predictable) "un" + "predictableInterfaceNames"
+ pkgs.lib.optionalString withNetworkd "-with-networkd"
+ pkgs.lib.optionalString systemdStage1 "-systemd-stage-1";
meta = {};
nodes.machine = { lib, ... }: {
nodes.machine = { lib, ... }: let
script = ''
ip link
if ${lib.optionalString predictable "!"} ip link show eth0; then
echo Success
else
exit 1
fi
'';
in {
networking.usePredictableInterfaceNames = lib.mkForce predictable;
networking.useNetworkd = withNetworkd;
networking.dhcpcd.enable = !withNetworkd;
networking.useDHCP = !withNetworkd;
# Check if predictable interface names are working in stage-1
boot.initrd.postDeviceCommands = ''
ip link
ip link show eth0 ${if predictable then "&&" else "||"} exit 1
'';
boot.initrd.postDeviceCommands = script;
boot.initrd.systemd = lib.mkIf systemdStage1 {
enable = true;
initrdBin = [ pkgs.iproute2 ];
services.systemd-udev-settle.wantedBy = ["initrd.target"];
services.check-interfaces = {
requiredBy = ["initrd.target"];
after = ["systemd-udev-settle.service"];
serviceConfig.Type = "oneshot";
path = [ pkgs.iproute2 ];
inherit script;
};
};
};
testScript = ''

View file

@ -0,0 +1,82 @@
import ./make-test-python.nix ({ lib, ... }: {
name = "systemd-initrd-network-ssh";
meta.maintainers = [ lib.maintainers.elvishjerricco ];
nodes = with lib; {
server = { config, pkgs, ... }: {
environment.systemPackages = [pkgs.cryptsetup];
boot.loader.systemd-boot.enable = true;
boot.loader.timeout = 0;
virtualisation = {
emptyDiskImages = [ 4096 ];
useBootLoader = true;
useEFIBoot = true;
};
specialisation.encrypted-root.configuration = {
virtualisation.bootDevice = "/dev/mapper/root";
boot.initrd.luks.devices = lib.mkVMOverride {
root.device = "/dev/vdc";
};
boot.initrd.systemd.enable = true;
boot.initrd.network = {
enable = true;
ssh = {
enable = true;
authorizedKeys = [ (readFile ./initrd-network-ssh/id_ed25519.pub) ];
port = 22;
# Terrible hack so it works with useBootLoader
hostKeys = [ { outPath = "${./initrd-network-ssh/ssh_host_ed25519_key}"; } ];
};
};
};
};
client = { config, ... }: {
environment.etc = {
knownHosts = {
text = concatStrings [
"server,"
"${
toString (head (splitString " " (toString
(elemAt (splitString "\n" config.networking.extraHosts) 2))))
} "
"${readFile ./initrd-network-ssh/ssh_host_ed25519_key.pub}"
];
};
sshKey = {
source = ./initrd-network-ssh/id_ed25519;
mode = "0600";
};
};
};
};
testScript = ''
start_all()
def ssh_is_up(_) -> bool:
status, _ = client.execute("nc -z server 22")
return status == 0
server.wait_for_unit("multi-user.target")
server.succeed(
"echo somepass | cryptsetup luksFormat --type=luks2 /dev/vdc",
"bootctl set-default nixos-generation-1-specialisation-encrypted-root.conf",
"sync",
)
server.shutdown()
server.start()
client.wait_for_unit("network.target")
with client.nested("waiting for SSH server to come up"):
retry(ssh_is_up)
client.succeed(
"echo somepass | ssh -i /etc/sshKey -o UserKnownHostsFile=/etc/knownHosts server 'systemd-tty-ask-password-agent' & exit"
)
server.wait_for_unit("multi-user.target")
server.succeed("mount | grep '/dev/mapper/root on /'")
'';
})

View file

@ -0,0 +1,74 @@
import ./make-test-python.nix ({ pkgs, lib, ... }: {
name = "systemd-initrd-network";
meta.maintainers = [ lib.maintainers.elvishjerricco ];
nodes = let
mkFlushTest = flush: script: { ... }: {
boot.initrd.systemd.enable = true;
boot.initrd.network = {
enable = true;
flushBeforeStage2 = flush;
};
systemd.services.check-flush = {
requiredBy = ["multi-user.target"];
before = ["network-pre.target" "multi-user.target"];
unitConfig.DefaultDependencies = false;
serviceConfig.Type = "oneshot";
path = [ pkgs.iproute2 pkgs.iputils pkgs.gnugrep ];
inherit script;
};
};
in {
basic = { ... }: {
boot.initrd.network.enable = true;
boot.initrd.systemd = {
enable = true;
# Enable network-online to fail the test in case of timeout
network.wait-online.timeout = 10;
network.wait-online.anyInterface = true;
targets.network-online.requiredBy = [ "initrd.target" ];
services.systemd-networkd-wait-online.requiredBy =
[ "network-online.target" ];
initrdBin = [ pkgs.iproute2 pkgs.iputils pkgs.gnugrep ];
services.check = {
requiredBy = [ "initrd.target" ];
before = [ "initrd.target" ];
after = [ "network-online.target" ];
serviceConfig.Type = "oneshot";
path = [ pkgs.iproute2 pkgs.iputils pkgs.gnugrep ];
script = ''
ip addr | grep 10.0.2.15 || exit 1
ping -c1 10.0.2.2 || exit 1
'';
};
};
};
doFlush = mkFlushTest true ''
if ip addr | grep 10.0.2.15; then
echo "Network configuration survived switch-root; flushBeforeStage2 failed"
exit 1
fi
'';
dontFlush = mkFlushTest false ''
if ! (ip addr | grep 10.0.2.15); then
echo "Network configuration didn't survive switch-root"
exit 1
fi
'';
};
testScript = ''
start_all()
basic.wait_for_unit("multi-user.target")
doFlush.wait_for_unit("multi-user.target")
dontFlush.wait_for_unit("multi-user.target")
# Make sure the systemd-network user was set correctly in initrd
basic.succeed("[ $(stat -c '%U,%G' /run/systemd/netif/links) = systemd-network,systemd-network ]")
basic.succeed("ip addr show >&2")
basic.succeed("ip route show >&2")
'';
})

View file

@ -27,6 +27,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
machine.succeed("[ -e /dev/pts/ptmx ]") # /dev/pts
machine.succeed("[ -e /run/keys ]") # /run/keys
with subtest("groups work"):
machine.fail("journalctl -b 0 | grep 'systemd-udevd.*Unknown group.*ignoring'")
with subtest("growfs works"):
oldAvail = machine.succeed("df --output=avail / | sed 1d")

View file

@ -78,14 +78,14 @@ in
STRIP = if strip then "${pkgsBuildHost.binutils.targetPrefix}strip" else null;
}) ''
mkdir ./root
mkdir -p ./root/var/empty
make-initrd-ng "$contentsPath" ./root
mkdir "$out"
(cd root && find * .[^.*] -exec touch -h -d '@1' '{}' +)
for PREP in $prepend; do
cat $PREP >> $out/initrd
done
(cd root && find * .[^.*] -print0 | sort -z | cpio -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd")
(cd root && find . -print0 | sort -z | cpio -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd")
if [ -n "$makeUInitrd" ]; then
mkimage -A "$uInitrdArch" -O linux -T ramdisk -C "$uInitrdCompression" -d "$out/initrd" $out/initrd.img