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