1{ config, lib, pkgs, utils, ... }:
2
3with utils;
4with lib;
5
6let
7
8 randomEncryptionCoerce = enable: { inherit enable; };
9
10 randomEncryptionOpts = { ... }: {
11
12 options = {
13
14 enable = mkOption {
15 default = false;
16 type = types.bool;
17 description = ''
18 Encrypt swap device with a random key. This way you won't have a persistent swap device.
19
20 WARNING: Don't try to hibernate when you have at least one swap partition with
21 this option enabled! We have no way to set the partition into which hibernation image
22 is saved, so if your image ends up on an encrypted one you would lose it!
23
24 WARNING #2: Do not use /dev/disk/by-uuid/… or /dev/disk/by-label/… as your swap device
25 when using randomEncryption as the UUIDs and labels will get erased on every boot when
26 the partition is encrypted. Best to use /dev/disk/by-partuuid/…
27 '';
28 };
29
30 cipher = mkOption {
31 default = "aes-xts-plain64";
32 example = "serpent-xts-plain64";
33 type = types.str;
34 description = ''
35 Use specified cipher for randomEncryption.
36
37 Hint: Run "cryptsetup benchmark" to see which one is fastest on your machine.
38 '';
39 };
40
41 keySize = mkOption {
42 default = null;
43 example = "512";
44 type = types.nullOr types.int;
45 description = ''
46 Set the encryption key size for the plain device.
47
48 If not specified, the amount of data to read from `source` will be
49 determined by cryptsetup.
50
51 See `cryptsetup-open(8)` for details.
52 '';
53 };
54
55 sectorSize = mkOption {
56 default = null;
57 example = "4096";
58 type = types.nullOr types.int;
59 description = ''
60 Set the sector size for the plain encrypted device type.
61
62 If not specified, the default sector size is determined from the
63 underlying block device.
64
65 See `cryptsetup-open(8)` for details.
66 '';
67 };
68
69 source = mkOption {
70 default = "/dev/urandom";
71 example = "/dev/random";
72 type = types.str;
73 description = ''
74 Define the source of randomness to obtain a random key for encryption.
75 '';
76 };
77
78 allowDiscards = mkOption {
79 default = false;
80 type = types.bool;
81 description = ''
82 Whether to allow TRIM requests to the underlying device. This option
83 has security implications; please read the LUKS documentation before
84 activating it.
85 '';
86 };
87 };
88
89 };
90
91 swapCfg = {config, options, ...}: {
92
93 options = {
94
95 device = mkOption {
96 example = "/dev/sda3";
97 type = types.nonEmptyStr;
98 description = "Path of the device or swap file.";
99 };
100
101 label = mkOption {
102 example = "swap";
103 type = types.str;
104 description = ''
105 Label of the device. Can be used instead of {var}`device`.
106 '';
107 };
108
109 size = mkOption {
110 default = null;
111 example = 2048;
112 type = types.nullOr types.int;
113 description = ''
114 If this option is set, ‘device’ is interpreted as the
115 path of a swapfile that will be created automatically
116 with the indicated size (in megabytes).
117 '';
118 };
119
120 priority = mkOption {
121 default = null;
122 example = 2048;
123 type = types.nullOr types.int;
124 description = ''
125 Specify the priority of the swap device. Priority is a value between 0 and 32767.
126 Higher numbers indicate higher priority.
127 null lets the kernel choose a priority, which will show up as a negative value.
128 '';
129 };
130
131 randomEncryption = mkOption {
132 default = false;
133 example = {
134 enable = true;
135 cipher = "serpent-xts-plain64";
136 source = "/dev/random";
137 };
138 type = types.coercedTo types.bool randomEncryptionCoerce (types.submodule randomEncryptionOpts);
139 description = ''
140 Encrypt swap device with a random key. This way you won't have a persistent swap device.
141
142 HINT: run "cryptsetup benchmark" to test cipher performance on your machine.
143
144 WARNING: Don't try to hibernate when you have at least one swap partition with
145 this option enabled! We have no way to set the partition into which hibernation image
146 is saved, so if your image ends up on an encrypted one you would lose it!
147
148 WARNING #2: Do not use /dev/disk/by-uuid/… or /dev/disk/by-label/… as your swap device
149 when using randomEncryption as the UUIDs and labels will get erased on every boot when
150 the partition is encrypted. Best to use /dev/disk/by-partuuid/…
151 '';
152 };
153
154 discardPolicy = mkOption {
155 default = null;
156 example = "once";
157 type = types.nullOr (types.enum ["once" "pages" "both" ]);
158 description = ''
159 Specify the discard policy for the swap device. If "once", then the
160 whole swap space is discarded at swapon invocation. If "pages",
161 asynchronous discard on freed pages is performed, before returning to
162 the available pages pool. With "both", both policies are activated.
163 See swapon(8) for more information.
164 '';
165 };
166
167 options = mkOption {
168 default = [ "defaults" ];
169 example = [ "nofail" ];
170 type = types.listOf types.nonEmptyStr;
171 description = ''
172 Options used to mount the swap.
173 '';
174 };
175
176 deviceName = mkOption {
177 type = types.str;
178 internal = true;
179 };
180
181 realDevice = mkOption {
182 type = types.path;
183 internal = true;
184 };
185
186 };
187
188 config = {
189 device = mkIf options.label.isDefined
190 "/dev/disk/by-label/${config.label}";
191 deviceName = lib.replaceStrings ["\\"] [""] (escapeSystemdPath config.device);
192 realDevice = if config.randomEncryption.enable then "/dev/mapper/${config.deviceName}" else config.device;
193 };
194
195 };
196
197in
198
199{
200
201 ###### interface
202
203 options = {
204
205 swapDevices = mkOption {
206 default = [];
207 example = [
208 { device = "/dev/hda7"; }
209 { device = "/var/swapfile"; }
210 { label = "bigswap"; }
211 ];
212 description = ''
213 The swap devices and swap files. These must have been
214 initialised using {command}`mkswap`. Each element
215 should be an attribute set specifying either the path of the
216 swap device or file (`device`) or the label
217 of the swap device (`label`, see
218 {command}`mkswap -L`). Using a label is
219 recommended.
220 '';
221
222 type = types.listOf (types.submodule swapCfg);
223 };
224
225 };
226
227 config = mkIf ((length config.swapDevices) != 0) {
228 assertions = map (sw: {
229 assertion = sw.randomEncryption.enable -> builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null;
230 message = ''
231 You cannot use swap device "${sw.device}" with randomEncryption enabled.
232 The UUIDs and labels will get erased on every boot when the partition is encrypted.
233 Use /dev/disk/by-partuuid/… instead.
234 '';
235 }) config.swapDevices;
236
237 warnings =
238 concatMap (sw:
239 if sw.size != null && hasPrefix "/dev/" sw.device
240 then [ "Setting the swap size of block device ${sw.device} has no effect" ]
241 else [ ])
242 config.swapDevices;
243
244 system.requiredKernelConfig = with config.lib.kernelConfig; [
245 (isYes "SWAP")
246 ];
247
248 # Create missing swapfiles.
249 systemd.services =
250 let
251 createSwapDevice = sw:
252 let realDevice' = escapeSystemdPath sw.realDevice;
253 in nameValuePair "mkswap-${sw.deviceName}"
254 { description = "Initialisation of swap device ${sw.device}";
255 # The mkswap service fails for file-backed swap devices if the
256 # loop module has not been loaded before the service runs.
257 # We add an ordering constraint to run after systemd-modules-load to
258 # avoid this race condition.
259 after = [ "systemd-modules-load.service" ];
260 wantedBy = [ "${realDevice'}.swap" ];
261 before = [ "${realDevice'}.swap" "shutdown.target"];
262 conflicts = [ "shutdown.target" ];
263 path = [ pkgs.util-linux pkgs.e2fsprogs ]
264 ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
265
266 environment.DEVICE = sw.device;
267
268 script =
269 ''
270 ${optionalString (sw.size != null) ''
271 currentSize=$(( $(stat -c "%s" "$DEVICE" 2>/dev/null || echo 0) / 1024 / 1024 ))
272 if [[ ! -b "$DEVICE" && "${toString sw.size}" != "$currentSize" ]]; then
273 # Disable CoW for CoW based filesystems like BTRFS.
274 truncate --size 0 "$DEVICE"
275 chattr +C "$DEVICE" 2>/dev/null || true
276
277 dd if=/dev/zero of="$DEVICE" bs=1M count=${toString sw.size}
278 chmod 0600 ${sw.device}
279 ${optionalString (!sw.randomEncryption.enable) "mkswap ${sw.realDevice}"}
280 fi
281 ''}
282 ${optionalString sw.randomEncryption.enable ''
283 cryptsetup plainOpen -c ${sw.randomEncryption.cipher} -d ${sw.randomEncryption.source} \
284 ${concatStringsSep " \\\n" (flatten [
285 (optional (sw.randomEncryption.sectorSize != null) "--sector-size=${toString sw.randomEncryption.sectorSize}")
286 (optional (sw.randomEncryption.keySize != null) "--key-size=${toString sw.randomEncryption.keySize}")
287 (optional sw.randomEncryption.allowDiscards "--allow-discards")
288 ])} ${sw.device} ${sw.deviceName}
289 mkswap ${sw.realDevice}
290 ''}
291 '';
292
293 unitConfig.RequiresMountsFor = [ "${dirOf sw.device}" ];
294 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
295 serviceConfig.Type = "oneshot";
296 serviceConfig.RemainAfterExit = sw.randomEncryption.enable;
297 serviceConfig.ExecStop = optionalString sw.randomEncryption.enable "${pkgs.cryptsetup}/bin/cryptsetup luksClose ${sw.deviceName}";
298 restartIfChanged = false;
299 };
300
301 in listToAttrs (map createSwapDevice (filter (sw: sw.size != null || sw.randomEncryption.enable) config.swapDevices));
302
303 };
304
305}