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}