1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11
12 cfg = config.services.keepalived;
13
14 keepalivedConf = pkgs.writeText "keepalived.conf" ''
15 global_defs {
16 ${optionalString cfg.enableScriptSecurity "enable_script_security"}
17 ${snmpGlobalDefs}
18 ${cfg.extraGlobalDefs}
19 }
20
21 ${vrrpScriptStr}
22 ${vrrpInstancesStr}
23 ${cfg.extraConfig}
24 '';
25
26 snmpGlobalDefs =
27 with cfg.snmp;
28 optionalString enable (
29 optionalString (socket != null) "snmp_socket ${socket}\n"
30 + optionalString enableKeepalived "enable_snmp_keepalived\n"
31 + optionalString enableChecker "enable_snmp_checker\n"
32 + optionalString enableRfc "enable_snmp_rfc\n"
33 + optionalString enableRfcV2 "enable_snmp_rfcv2\n"
34 + optionalString enableRfcV3 "enable_snmp_rfcv3\n"
35 + optionalString enableTraps "enable_traps"
36 );
37
38 vrrpScriptStr = concatStringsSep "\n" (
39 map (s: ''
40 vrrp_script ${s.name} {
41 script "${s.script}"
42 interval ${toString s.interval}
43 fall ${toString s.fall}
44 rise ${toString s.rise}
45 timeout ${toString s.timeout}
46 weight ${toString s.weight}
47 user ${s.user} ${optionalString (s.group != null) s.group}
48
49 ${s.extraConfig}
50 }
51 '') vrrpScripts
52 );
53
54 vrrpInstancesStr = concatStringsSep "\n" (
55 map (i: ''
56 vrrp_instance ${i.name} {
57 interface ${i.interface}
58 state ${i.state}
59 virtual_router_id ${toString i.virtualRouterId}
60 priority ${toString i.priority}
61 ${optionalString i.noPreempt "nopreempt"}
62
63 ${optionalString i.useVmac (
64 "use_vmac" + optionalString (i.vmacInterface != null) " ${i.vmacInterface}"
65 )}
66 ${optionalString i.vmacXmitBase "vmac_xmit_base"}
67
68 ${optionalString (i.unicastSrcIp != null) "unicast_src_ip ${i.unicastSrcIp}"}
69 ${optionalString (builtins.length i.unicastPeers > 0) ''
70 unicast_peer {
71 ${concatStringsSep "\n" i.unicastPeers}
72 }
73 ''}
74
75 virtual_ipaddress {
76 ${concatMapStringsSep "\n" virtualIpLine i.virtualIps}
77 }
78
79 ${optionalString (builtins.length i.trackScripts > 0) ''
80 track_script {
81 ${concatStringsSep "\n" i.trackScripts}
82 }
83 ''}
84
85 ${optionalString (builtins.length i.trackInterfaces > 0) ''
86 track_interface {
87 ${concatStringsSep "\n" i.trackInterfaces}
88 }
89 ''}
90
91 ${i.extraConfig}
92 }
93 '') vrrpInstances
94 );
95
96 virtualIpLine =
97 ip:
98 ip.addr
99 + optionalString (notNullOrEmpty ip.brd) " brd ${ip.brd}"
100 + optionalString (notNullOrEmpty ip.dev) " dev ${ip.dev}"
101 + optionalString (notNullOrEmpty ip.scope) " scope ${ip.scope}"
102 + optionalString (notNullOrEmpty ip.label) " label ${ip.label}";
103
104 notNullOrEmpty = s: !(s == null || s == "");
105
106 vrrpScripts = mapAttrsToList (
107 name: config:
108 {
109 inherit name;
110 }
111 // config
112 ) cfg.vrrpScripts;
113
114 vrrpInstances = mapAttrsToList (
115 iName: iConfig:
116 {
117 name = iName;
118 }
119 // iConfig
120 ) cfg.vrrpInstances;
121
122 vrrpInstanceAssertions =
123 i:
124 [
125 {
126 assertion = i.interface != "";
127 message = "services.keepalived.vrrpInstances.${i.name}.interface option cannot be empty.";
128 }
129 {
130 assertion = i.virtualRouterId >= 0 && i.virtualRouterId <= 255;
131 message = "services.keepalived.vrrpInstances.${i.name}.virtualRouterId must be an integer between 0..255.";
132 }
133 {
134 assertion = i.priority >= 0 && i.priority <= 255;
135 message = "services.keepalived.vrrpInstances.${i.name}.priority must be an integer between 0..255.";
136 }
137 {
138 assertion = i.vmacInterface == null || i.useVmac;
139 message = "services.keepalived.vrrpInstances.${i.name}.vmacInterface has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set.";
140 }
141 {
142 assertion = !i.vmacXmitBase || i.useVmac;
143 message = "services.keepalived.vrrpInstances.${i.name}.vmacXmitBase has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set.";
144 }
145 ]
146 ++ flatten (map (virtualIpAssertions i.name) i.virtualIps)
147 ++ flatten (map (vrrpScriptAssertion i.name) i.trackScripts);
148
149 virtualIpAssertions = vrrpName: ip: [
150 {
151 assertion = ip.addr != "";
152 message = "The 'addr' option for an services.keepalived.vrrpInstances.${vrrpName}.virtualIps entry cannot be empty.";
153 }
154 ];
155
156 vrrpScriptAssertion = vrrpName: scriptName: {
157 assertion = builtins.hasAttr scriptName cfg.vrrpScripts;
158 message = "services.keepalived.vrrpInstances.${vrrpName} trackscript ${scriptName} is not defined in services.keepalived.vrrpScripts.";
159 };
160
161 pidFile = "/run/keepalived.pid";
162
163in
164{
165 meta.maintainers = [ lib.maintainers.raitobezarius ];
166
167 options = {
168 services.keepalived = {
169
170 enable = mkOption {
171 type = types.bool;
172 default = false;
173 description = ''
174 Whether to enable Keepalived.
175 '';
176 };
177
178 package = lib.mkPackageOption pkgs "keepalived" { };
179
180 openFirewall = mkOption {
181 type = types.bool;
182 default = false;
183 description = ''
184 Whether to automatically allow VRRP and AH packets in the firewall.
185 '';
186 };
187
188 enableScriptSecurity = mkOption {
189 type = types.bool;
190 default = false;
191 description = ''
192 Don't run scripts configured to be run as root if any part of the path is writable by a non-root user.
193 '';
194 };
195
196 snmp = {
197
198 enable = mkOption {
199 type = types.bool;
200 default = false;
201 description = ''
202 Whether to enable the builtin AgentX subagent.
203 '';
204 };
205
206 socket = mkOption {
207 type = types.nullOr types.str;
208 default = null;
209 description = ''
210 Socket to use for connecting to SNMP master agent. If this value is
211 set to null, keepalived's default will be used, which is
212 unix:/var/agentx/master, unless using a network namespace, when the
213 default is udp:localhost:705.
214 '';
215 };
216
217 enableKeepalived = mkOption {
218 type = types.bool;
219 default = false;
220 description = ''
221 Enable SNMP handling of vrrp element of KEEPALIVED MIB.
222 '';
223 };
224
225 enableChecker = mkOption {
226 type = types.bool;
227 default = false;
228 description = ''
229 Enable SNMP handling of checker element of KEEPALIVED MIB.
230 '';
231 };
232
233 enableRfc = mkOption {
234 type = types.bool;
235 default = false;
236 description = ''
237 Enable SNMP handling of RFC2787 and RFC6527 VRRP MIBs.
238 '';
239 };
240
241 enableRfcV2 = mkOption {
242 type = types.bool;
243 default = false;
244 description = ''
245 Enable SNMP handling of RFC2787 VRRP MIB.
246 '';
247 };
248
249 enableRfcV3 = mkOption {
250 type = types.bool;
251 default = false;
252 description = ''
253 Enable SNMP handling of RFC6527 VRRP MIB.
254 '';
255 };
256
257 enableTraps = mkOption {
258 type = types.bool;
259 default = false;
260 description = ''
261 Enable SNMP traps.
262 '';
263 };
264
265 };
266
267 vrrpScripts = mkOption {
268 type = types.attrsOf (
269 types.submodule (
270 import ./vrrp-script-options.nix {
271 inherit lib;
272 }
273 )
274 );
275 default = { };
276 description = "Declarative vrrp script config";
277 };
278
279 vrrpInstances = mkOption {
280 type = types.attrsOf (
281 types.submodule (
282 import ./vrrp-instance-options.nix {
283 inherit lib;
284 }
285 )
286 );
287 default = { };
288 description = "Declarative vhost config";
289 };
290
291 extraGlobalDefs = mkOption {
292 type = types.lines;
293 default = "";
294 description = ''
295 Extra lines to be added verbatim to the 'global_defs' block of the
296 configuration file
297 '';
298 };
299
300 extraConfig = mkOption {
301 type = types.lines;
302 default = "";
303 description = ''
304 Extra lines to be added verbatim to the configuration file.
305 '';
306 };
307
308 secretFile = mkOption {
309 type = types.nullOr types.path;
310 default = null;
311 example = "/run/keys/keepalived.env";
312 description = ''
313 Environment variables from this file will be interpolated into the
314 final config file using envsubst with this syntax: `$ENVIRONMENT`
315 or `''${VARIABLE}`.
316 The file should contain lines formatted as `SECRET_VAR=SECRET_VALUE`.
317 This is useful to avoid putting secrets into the nix store.
318 '';
319 };
320
321 };
322 };
323
324 config = mkIf cfg.enable {
325
326 assertions = flatten (map vrrpInstanceAssertions vrrpInstances);
327
328 networking.firewall = lib.mkIf cfg.openFirewall {
329 extraCommands = ''
330 # Allow VRRP and AH packets
331 ip46tables -A nixos-fw -p vrrp -m comment --comment "services.keepalived.openFirewall" -j ACCEPT
332 ip46tables -A nixos-fw -p ah -m comment --comment "services.keepalived.openFirewall" -j ACCEPT
333 '';
334
335 extraStopCommands = ''
336 ip46tables -D nixos-fw -p vrrp -m comment --comment "services.keepalived.openFirewall" -j ACCEPT
337 ip46tables -D nixos-fw -p ah -m comment --comment "services.keepalived.openFirewall" -j ACCEPT
338 '';
339 };
340
341 systemd.timers.keepalived-boot-delay = {
342 description = "Keepalive Daemon delay to avoid instant transition to MASTER state";
343 after = [
344 "network.target"
345 "network-online.target"
346 ];
347 requires = [ "network-online.target" ];
348 wantedBy = [ "multi-user.target" ];
349 timerConfig = {
350 OnActiveSec = "5s";
351 Unit = "keepalived.service";
352 };
353 };
354
355 systemd.services.keepalived =
356 let
357 finalConfigFile =
358 if cfg.secretFile == null then keepalivedConf else "/run/keepalived/keepalived.conf";
359 in
360 {
361 description = "Keepalive Daemon (LVS and VRRP)";
362 after = [
363 "network.target"
364 "network-online.target"
365 ];
366 wants = [ "network-online.target" ];
367 serviceConfig = {
368 Type = "forking";
369 PIDFile = pidFile;
370 KillMode = "process";
371 RuntimeDirectory = "keepalived";
372 EnvironmentFile = lib.optional (cfg.secretFile != null) cfg.secretFile;
373 ExecStartPre = lib.optional (cfg.secretFile != null) (
374 pkgs.writeShellScript "keepalived-pre-start" ''
375 umask 077
376 ${pkgs.envsubst}/bin/envsubst -i "${keepalivedConf}" > ${finalConfigFile}
377 ''
378 );
379 ExecStart =
380 "${lib.getExe cfg.package}"
381 + " -f ${finalConfigFile}"
382 + " -p ${pidFile}"
383 + optionalString cfg.snmp.enable " --snmp";
384 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
385 Restart = "always";
386 RestartSec = "1s";
387 };
388 };
389 };
390}