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 megabytes).
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 in
272 lib.nameValuePair "mkswap-${sw.deviceName}" {
273 description = "Initialisation of swap device ${sw.device}";
274 # The mkswap service fails for file-backed swap devices if the
275 # loop module has not been loaded before the service runs.
276 # We add an ordering constraint to run after systemd-modules-load to
277 # avoid this race condition.
278 after = [ "systemd-modules-load.service" ];
279 wantedBy = [ "${realDevice'}.swap" ];
280 before = [
281 "${realDevice'}.swap"
282 "shutdown.target"
283 ];
284 conflicts = [ "shutdown.target" ];
285 path = [
286 pkgs.util-linux
287 pkgs.e2fsprogs
288 ] ++ lib.optional sw.randomEncryption.enable pkgs.cryptsetup;
289
290 environment.DEVICE = sw.device;
291
292 script = ''
293 ${lib.optionalString (sw.size != null) ''
294 currentSize=$(( $(stat -c "%s" "$DEVICE" 2>/dev/null || echo 0) / 1024 / 1024 ))
295 if [[ ! -b "$DEVICE" && "${toString sw.size}" != "$currentSize" ]]; then
296 # Disable CoW for CoW based filesystems like BTRFS.
297 truncate --size 0 "$DEVICE"
298 chattr +C "$DEVICE" 2>/dev/null || true
299
300 echo "Creating swap file using dd and mkswap."
301 dd if=/dev/zero of="$DEVICE" bs=1M count=${toString sw.size} status=progress
302 ${lib.optionalString (!sw.randomEncryption.enable) "mkswap ${sw.realDevice}"}
303 fi
304 ''}
305 ${lib.optionalString sw.randomEncryption.enable ''
306 cryptsetup plainOpen -c ${sw.randomEncryption.cipher} -d ${sw.randomEncryption.source} \
307 ${
308 lib.concatStringsSep " \\\n" (
309 lib.flatten [
310 (lib.optional (
311 sw.randomEncryption.sectorSize != null
312 ) "--sector-size=${toString sw.randomEncryption.sectorSize}")
313 (lib.optional (
314 sw.randomEncryption.keySize != null
315 ) "--key-size=${toString sw.randomEncryption.keySize}")
316 (lib.optional sw.randomEncryption.allowDiscards "--allow-discards")
317 ]
318 )
319 } ${sw.device} ${sw.deviceName}
320 mkswap ${sw.realDevice}
321 ''}
322 '';
323
324 unitConfig.RequiresMountsFor = [ "${dirOf sw.device}" ];
325 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
326 serviceConfig = {
327 Type = "oneshot";
328 RemainAfterExit = sw.randomEncryption.enable;
329 UMask = "0177";
330 ExecStop = lib.optionalString sw.randomEncryption.enable "${pkgs.cryptsetup}/bin/cryptsetup luksClose ${sw.deviceName}";
331 };
332 restartIfChanged = false;
333 };
334
335 in
336 lib.listToAttrs (
337 lib.map createSwapDevice (
338 lib.filter (sw: sw.size != null || sw.randomEncryption.enable) config.swapDevices
339 )
340 );
341
342 };
343
344}