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}