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