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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc "Path of the device or swap file.";
99 };
100
101 label = mkOption {
102 example = "swap";
103 type = types.str;
104 description = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 wantedBy = [ "${realDevice'}.swap" ];
256 before = [ "${realDevice'}.swap" ];
257 path = [ pkgs.util-linux pkgs.e2fsprogs ]
258 ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
259
260 environment.DEVICE = sw.device;
261
262 script =
263 ''
264 ${optionalString (sw.size != null) ''
265 currentSize=$(( $(stat -c "%s" "$DEVICE" 2>/dev/null || echo 0) / 1024 / 1024 ))
266 if [[ ! -b "$DEVICE" && "${toString sw.size}" != "$currentSize" ]]; then
267 # Disable CoW for CoW based filesystems like BTRFS.
268 truncate --size 0 "$DEVICE"
269 chattr +C "$DEVICE" 2>/dev/null || true
270
271 dd if=/dev/zero of="$DEVICE" bs=1M count=${toString sw.size}
272 chmod 0600 ${sw.device}
273 ${optionalString (!sw.randomEncryption.enable) "mkswap ${sw.realDevice}"}
274 fi
275 ''}
276 ${optionalString sw.randomEncryption.enable ''
277 cryptsetup plainOpen -c ${sw.randomEncryption.cipher} -d ${sw.randomEncryption.source} \
278 ${concatStringsSep " \\\n" (flatten [
279 (optional (sw.randomEncryption.sectorSize != null) "--sector-size=${toString sw.randomEncryption.sectorSize}")
280 (optional (sw.randomEncryption.keySize != null) "--key-size=${toString sw.randomEncryption.keySize}")
281 (optional sw.randomEncryption.allowDiscards "--allow-discards")
282 ])} ${sw.device} ${sw.deviceName}
283 mkswap ${sw.realDevice}
284 ''}
285 '';
286
287 unitConfig.RequiresMountsFor = [ "${dirOf sw.device}" ];
288 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
289 serviceConfig.Type = "oneshot";
290 serviceConfig.RemainAfterExit = sw.randomEncryption.enable;
291 serviceConfig.ExecStop = optionalString sw.randomEncryption.enable "${pkgs.cryptsetup}/bin/cryptsetup luksClose ${sw.deviceName}";
292 restartIfChanged = false;
293 };
294
295 in listToAttrs (map createSwapDevice (filter (sw: sw.size != null || sw.randomEncryption.enable) config.swapDevices));
296
297 };
298
299}