diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix
index 4aceaad9e9fb..52ad71a66c79 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/modules/system/boot/systemd-unit-options.nix
@@ -4,15 +4,184 @@ with lib;
let
- checkService = v:
- let assertValueOneOf = name: values: attr:
- let val = attr.${name};
- in optional (attr ? ${name} && !elem val values) "Systemd service field `${name}' cannot have value `${val}'.";
- checkType = assertValueOneOf "Type" ["simple" "forking" "oneshot" "dbus" "notify" "idle"];
- checkRestart = assertValueOneOf "Restart" ["no" "on-success" "on-failure" "on-abort" "always"];
- errors = concatMap (c: c v) [checkType checkRestart];
- in if errors == [] then true
- else builtins.trace (concatStringsSep "\n" errors) false;
+ boolValues = [true false "yes" "no"];
+
+ assertValueOneOf = name: values: group: attr:
+ optional (attr ? ${name} && !elem attr.${name} values)
+ "Systemd ${group} field `${name}' cannot have value `${attr.${name}}'.";
+
+ assertHasField = name: group: attr:
+ optional (!(attr ? ${name}))
+ "Systemd ${group} field `${name}' must exist.";
+
+ assertOnlyFields = fields: group: attr:
+ let badFields = filter (name: ! elem name fields) (attrNames attr); in
+ optional (badFields != [ ])
+ "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
+
+ assertRange = name: min: max: group: attr:
+ optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
+ "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
+
+ digits = map toString (range 0 9);
+
+ isByteFormat = s:
+ let
+ l = reverseList (stringToCharacters s);
+ suffix = head l;
+ nums = tail l;
+ in elem suffix (["K" "M" "G" "T"] ++ digits)
+ && all (num: elem num digits) nums;
+
+ assertByteFormat = name: group: attr:
+ optional (attr ? ${name} && ! isByteFormat attr.${name})
+ "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
+
+ hexChars = stringToCharacters "0123456789abcdefABCDEF";
+
+ isMacAddress = s: stringLength s == 17
+ && flip all (splitString ":" s) (bytes:
+ all (byte: elem byte hexChars) (stringToCharacters bytes)
+ );
+
+ assertMacAddress = name: group: attr:
+ optional (attr ? ${name} && ! isMacAddress attr.${name})
+ "Systemd ${group} field `${name}' must be a valid mac address.";
+
+ checkUnitConfig = group: checks: v:
+ let errors = concatMap (c: c group v) checks; in
+ if errors == [] then true
+ else builtins.trace (concatStringsSep "\n" errors) false;
+
+ checkService = checkUnitConfig "Service" [
+ (assertValueOneOf "Type" [
+ "simple" "forking" "oneshot" "dbus" "notify" "idle"
+ ])
+ (assertValueOneOf "Restart" [
+ "no" "on-success" "on-failure" "on-abort" "always"
+ ])
+ ];
+
+ checkLink = checkUnitConfig "Link" [
+ (assertOnlyFields [
+ "Description" "Alias" "MACAddressPolicy" "MACAddress" "NamePolicy" "Name"
+ "MTUBytes" "BitsPerSecond" "Duplex" "WakeOnLan"
+ ])
+ (assertValueOneOf "MACAddressPolicy" ["persistent" "random"])
+ (assertMacAddress "MACAddress")
+ (assertValueOneOf "NamePolicy" [
+ "kernel" "database" "onboard" "slot" "path" "mac"
+ ])
+ (assertByteFormat "MTUBytes")
+ (assertByteFormat "BitsPerSecond")
+ (assertValueOneOf "Duplex" ["half" "full"])
+ (assertValueOneOf "WakeOnLan" ["phy" "magic" "off"])
+ ];
+
+ checkNetdev = checkUnitConfig "Netdev" [
+ (assertOnlyFields [
+ "Description" "Name" "Kind" "MTUBytes" "MACAddress"
+ ])
+ (assertHasField "Name")
+ (assertHasField "Kind")
+ (assertValueOneOf "Kind" [
+ "bridge" "bond" "vlan" "macvlan" "vxlan" "ipip"
+ "gre" "sit" "vti" "veth" "tun" "tap" "dummy"
+ ])
+ (assertByteFormat "MTUBytes")
+ (assertMacAddress "MACAddress")
+ ];
+
+ checkVlan = checkUnitConfig "VLAN" [
+ (assertOnlyFields ["Id"])
+ (assertRange "Id" 0 4094)
+ ];
+
+ checkMacvlan = checkUnitConfig "MACVLAN" [
+ (assertOnlyFields ["Mode"])
+ (assertValueOneOf "Mode" ["private" "vepa" "bridge" "passthru"])
+ ];
+
+ checkVxlan = checkUnitConfig "VXLAN" [
+ (assertOnlyFields ["Id" "Group" "TOS" "TTL" "MacLearning"])
+ (assertRange "TTL" 0 255)
+ (assertValueOneOf "MacLearning" boolValues)
+ ];
+
+ checkTunnel = checkUnitConfig "Tunnel" [
+ (assertOnlyFields ["Local" "Remote" "TOS" "TTL" "DiscoverPathMTU"])
+ (assertRange "TTL" 0 255)
+ (assertValueOneOf "DiscoverPathMTU" boolValues)
+ ];
+
+ checkPeer = checkUnitConfig "Peer" [
+ (assertOnlyFields ["Name" "MACAddress"])
+ (assertMacAddress "MACAddress")
+ ];
+
+ tunTapChecks = [
+ (assertOnlyFields ["OneQueue" "MultiQueue" "PacketInfo" "User" "Group"])
+ (assertValueOneOf "OneQueue" boolValues)
+ (assertValueOneOf "MultiQueue" boolValues)
+ (assertValueOneOf "PacketInfo" boolValues)
+ ];
+
+ checkTun = checkUnitConfig "Tun" tunTapChecks;
+
+ checkTap = checkUnitConfig "Tap" tunTapChecks;
+
+ checkBond = checkUnitConfig "Bond" [
+ (assertOnlyFields [
+ "Mode" "TransmitHashPolicy" "LACPTransmitRate" "MIIMonitorSec"
+ "UpDelaySec" "DownDelaySec"
+ ])
+ (assertValueOneOf "Mode" [
+ "balance-rr" "active-backup" "balance-xor"
+ "broadcast" "802.3ad" "balance-tlb" "balance-alb"
+ ])
+ (assertValueOneOf "TransmitHashPolicy" [
+ "layer2" "layer3+4" "layer2+3" "encap2+3" "802.3ad" "encap3+4"
+ ])
+ (assertValueOneOf "LACPTransmitRate" ["slow" "fast"])
+ ];
+
+ checkNetwork = checkUnitConfig "Network" [
+ (assertOnlyFields [
+ "Description" "DHCP" "DHCPServer" "IPv4LL" "IPv4LLRoute"
+ "LLMNR" "Domains" "Bridge" "Bond"
+ ])
+ (assertValueOneOf "DHCP" ["both" "none" "v4" "v6"])
+ (assertValueOneOf "DHCPServer" boolValues)
+ (assertValueOneOf "IPv4LL" boolValues)
+ (assertValueOneOf "IPv4LLRoute" boolValues)
+ (assertValueOneOf "LLMNR" boolValues)
+ ];
+
+ checkAddress = checkUnitConfig "Address" [
+ (assertOnlyFields ["Address" "Peer" "Broadcast" "Label"])
+ (assertHasField "Address")
+ ];
+
+ checkRoute = checkUnitConfig "Route" [
+ (assertOnlyFields ["Gateway" "Destination" "Metric"])
+ (assertHasField "Gateway")
+ ];
+
+ checkDhcp = checkUnitConfig "DHCP" [
+ (assertOnlyFields [
+ "UseDNS" "UseMTU" "SendHostname" "UseHostname" "UseDomains" "UseRoutes"
+ "CriticalConnections" "VendorClassIdentifier" "RequestBroadcast"
+ "RouteMetric"
+ ])
+ (assertValueOneOf "UseDNS" boolValues)
+ (assertValueOneOf "UseMTU" boolValues)
+ (assertValueOneOf "SendHostname" boolValues)
+ (assertValueOneOf "UseHostname" boolValues)
+ (assertValueOneOf "UseDomains" boolValues)
+ (assertValueOneOf "UseRoutes" boolValues)
+ (assertValueOneOf "CriticalConnections" boolValues)
+ (assertValueOneOf "RequestBroadcast" boolValues)
+ ];
unitOption = mkOptionType {
name = "systemd option";
@@ -482,7 +651,7 @@ in rec {
linkConfig = mkOption {
default = {};
example = { MACAddress = "00:ff:ee:aa:cc:dd"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkLink;
description = ''
Each attribute in this set specifies an option in the
[Link] section of the unit. See
@@ -498,7 +667,7 @@ in rec {
netdevConfig = mkOption {
default = {};
example = { Name = "mybridge"; Kind = "bridge"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkNetdev;
description = ''
Each attribute in this set specifies an option in the
[Netdev] section of the unit. See
@@ -510,7 +679,7 @@ in rec {
vlanConfig = mkOption {
default = {};
example = { Id = "4"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkVlan;
description = ''
Each attribute in this set specifies an option in the
[VLAN] section of the unit. See
@@ -522,7 +691,7 @@ in rec {
macvlanConfig = mkOption {
default = {};
example = { Mode = "private"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkMacvlan;
description = ''
Each attribute in this set specifies an option in the
[MACVLAN] section of the unit. See
@@ -534,7 +703,7 @@ in rec {
vxlanConfig = mkOption {
default = {};
example = { Id = "4"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkVxlan;
description = ''
Each attribute in this set specifies an option in the
[VXLAN] section of the unit. See
@@ -546,7 +715,7 @@ in rec {
tunnelConfig = mkOption {
default = {};
example = { Remote = "192.168.1.1"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkTunnel;
description = ''
Each attribute in this set specifies an option in the
[Tunnel] section of the unit. See
@@ -558,7 +727,7 @@ in rec {
peerConfig = mkOption {
default = {};
example = { Name = "veth2"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkPeer;
description = ''
Each attribute in this set specifies an option in the
[Peer] section of the unit. See
@@ -570,7 +739,7 @@ in rec {
tunConfig = mkOption {
default = {};
example = { User = "openvpn"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkTun;
description = ''
Each attribute in this set specifies an option in the
[Tun] section of the unit. See
@@ -582,7 +751,7 @@ in rec {
tapConfig = mkOption {
default = {};
example = { User = "openvpn"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkTap;
description = ''
Each attribute in this set specifies an option in the
[Tap] section of the unit. See
@@ -594,7 +763,7 @@ in rec {
bondConfig = mkOption {
default = {};
example = { Mode = "802.3ad"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkBond;
description = ''
Each attribute in this set specifies an option in the
[Bond] section of the unit. See
@@ -610,7 +779,7 @@ in rec {
addressConfig = mkOption {
default = {};
example = { Address = "192.168.0.100/24"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkAddress;
description = ''
Each attribute in this set specifies an option in the
[Address] section of the unit. See
@@ -626,7 +795,7 @@ in rec {
routeConfig = mkOption {
default = {};
example = { Gateway = "192.168.0.1"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkRoute;
description = ''
Each attribute in this set specifies an option in the
[Route] section of the unit. See
@@ -642,7 +811,7 @@ in rec {
networkConfig = mkOption {
default = {};
example = { Description = "My Network"; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkNetwork;
description = ''
Each attribute in this set specifies an option in the
[Network] section of the unit. See
@@ -654,7 +823,7 @@ in rec {
dhcpConfig = mkOption {
default = {};
example = { UseDNS = true; UseRoutes = true; };
- type = types.attrsOf unitOption;
+ type = types.addCheck (types.attrsOf unitOption) checkDhcp;
description = ''
Each attribute in this set specifies an option in the
[DHCP] section of the unit. See