at master 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 12 cfg = config.services.znc; 13 14 defaultUser = "znc"; 15 16 modules = pkgs.buildEnv { 17 name = "znc-modules"; 18 paths = cfg.modulePackages; 19 }; 20 21 listenerPorts = concatMap (l: optional (l ? Port) l.Port) (attrValues (cfg.config.Listener or { })); 22 23 # Converts the config option to a string 24 semanticString = 25 let 26 27 sortedAttrs = 28 set: 29 sort ( 30 l: r: 31 if l == "extraConfig" then 32 false # Always put extraConfig last 33 else if isAttrs set.${l} == isAttrs set.${r} then 34 l < r 35 else 36 isAttrs set.${r} # Attrsets should be last, makes for a nice config 37 # This last case occurs when any side (but not both) is an attrset 38 # The order of these is correct when the attrset is on the right 39 # which we're just returning 40 ) (attrNames set); 41 42 # Specifies an attrset that encodes the value according to its type 43 encode = 44 name: value: 45 { 46 null = [ ]; 47 bool = [ "${name} = ${boolToString value}" ]; 48 int = [ "${name} = ${toString value}" ]; 49 50 # extraConfig should be inserted verbatim 51 string = [ (if name == "extraConfig" then value else "${name} = ${value}") ]; 52 53 # Values like `Foo = [ "bar" "baz" ];` should be transformed into 54 # Foo=bar 55 # Foo=baz 56 list = concatMap (encode name) value; 57 58 # Values like `Foo = { bar = { Baz = "baz"; Qux = "qux"; Florps = null; }; };` should be transmed into 59 # <Foo bar> 60 # Baz=baz 61 # Qux=qux 62 # </Foo> 63 set = concatMap ( 64 subname: 65 optionals (value.${subname} != null) ( 66 [ 67 "<${name} ${subname}>" 68 ] 69 ++ map (line: "\t${line}") (toLines value.${subname}) 70 ++ [ 71 "</${name}>" 72 ] 73 ) 74 ) (filter (v: v != null) (attrNames value)); 75 76 } 77 .${builtins.typeOf value}; 78 79 # One level "above" encode, acts upon a set and uses encode on each name,value pair 80 toLines = set: concatMap (name: encode name set.${name}) (sortedAttrs set); 81 82 in 83 concatStringsSep "\n" (toLines cfg.config); 84 85 semanticTypes = with types; rec { 86 zncAtom = nullOr (oneOf [ 87 int 88 bool 89 str 90 ]); 91 zncAttr = attrsOf (nullOr zncConf); 92 zncAll = oneOf [ 93 zncAtom 94 (listOf zncAtom) 95 zncAttr 96 ]; 97 zncConf = attrsOf ( 98 zncAll 99 // { 100 # Since this is a recursive type and the description by default contains 101 # the description of its subtypes, infinite recursion would occur without 102 # explicitly breaking this cycle 103 description = "znc values (null, atoms (str, int, bool), list of atoms, or attrsets of znc values)"; 104 } 105 ); 106 }; 107 108in 109 110{ 111 112 imports = [ ./options.nix ]; 113 114 options = { 115 services.znc = { 116 enable = mkEnableOption "ZNC"; 117 118 user = mkOption { 119 default = "znc"; 120 example = "john"; 121 type = types.str; 122 description = '' 123 The name of an existing user account to use to own the ZNC server 124 process. If not specified, a default user will be created. 125 ''; 126 }; 127 128 group = mkOption { 129 default = defaultUser; 130 example = "users"; 131 type = types.str; 132 description = '' 133 Group to own the ZNC process. 134 ''; 135 }; 136 137 dataDir = mkOption { 138 default = "/var/lib/znc"; 139 example = "/home/john/.znc"; 140 type = types.path; 141 description = '' 142 The state directory for ZNC. The config and the modules will be linked 143 to from this directory as well. 144 ''; 145 }; 146 147 openFirewall = mkOption { 148 type = types.bool; 149 default = false; 150 description = '' 151 Whether to open ports in the firewall for ZNC. Does work with 152 ports for listeners specified in 153 {option}`services.znc.config.Listener`. 154 ''; 155 }; 156 157 config = mkOption { 158 type = semanticTypes.zncConf; 159 default = { }; 160 example = literalExpression '' 161 { 162 LoadModule = [ "webadmin" "adminlog" ]; 163 User.paul = { 164 Admin = true; 165 Nick = "paul"; 166 AltNick = "paul1"; 167 LoadModule = [ "chansaver" "controlpanel" ]; 168 Network.libera = { 169 Server = "irc.libera.chat +6697"; 170 LoadModule = [ "simple_away" ]; 171 Chan = { 172 "#nixos" = { Detached = false; }; 173 "##linux" = { Disabled = true; }; 174 }; 175 }; 176 Pass.password = { 177 Method = "sha256"; 178 Hash = "e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93"; 179 Salt = "l5Xryew4g*!oa(ECfX2o"; 180 }; 181 }; 182 } 183 ''; 184 description = '' 185 Configuration for ZNC, see 186 <https://wiki.znc.in/Configuration> for details. The 187 Nix value declared here will be translated directly to the xml-like 188 format ZNC expects. This is much more flexible than the legacy options 189 under {option}`services.znc.confOptions.*`, but also can't do 190 any type checking. 191 192 You can use {command}`nix-instantiate --eval --strict '<nixpkgs/nixos>' -A config.services.znc.config` 193 to view the current value. By default it contains a listener for port 194 5000 with SSL enabled. 195 196 Nix attributes called `extraConfig` will be inserted 197 verbatim into the resulting config file. 198 199 If {option}`services.znc.useLegacyConfig` is turned on, the 200 option values in {option}`services.znc.confOptions.*` will be 201 gracefully be applied to this option. 202 203 If you intend to update the configuration through this option, be sure 204 to disable {option}`services.znc.mutable`, otherwise none of the 205 changes here will be applied after the initial deploy. 206 ''; 207 }; 208 209 configFile = mkOption { 210 type = types.path; 211 example = literalExpression "~/.znc/configs/znc.conf"; 212 description = '' 213 Configuration file for ZNC. It is recommended to use the 214 {option}`config` option instead. 215 216 Setting this option will override any auto-generated config file 217 through the {option}`confOptions` or {option}`config` 218 options. 219 ''; 220 }; 221 222 modulePackages = mkOption { 223 type = types.listOf types.package; 224 default = [ ]; 225 example = literalExpression "[ pkgs.zncModules.fish pkgs.zncModules.push ]"; 226 description = '' 227 A list of global znc module packages to add to znc. 228 ''; 229 }; 230 231 mutable = mkOption { 232 default = true; # TODO: Default to true when config is set, make sure to not delete the old config if present 233 type = types.bool; 234 description = '' 235 Indicates whether to allow the contents of the 236 `dataDir` directory to be changed by the user at 237 run-time. 238 239 If enabled, modifications to the ZNC configuration after its initial 240 creation are not overwritten by a NixOS rebuild. If disabled, the 241 ZNC configuration is rebuilt on every NixOS rebuild. 242 243 If the user wants to manage the ZNC service using the web admin 244 interface, this option should be enabled. 245 ''; 246 }; 247 248 extraFlags = mkOption { 249 default = [ ]; 250 example = [ "--debug" ]; 251 type = types.listOf types.str; 252 description = '' 253 Extra arguments to use for executing znc. 254 ''; 255 }; 256 }; 257 }; 258 259 ###### Implementation 260 261 config = mkIf cfg.enable { 262 263 services.znc = { 264 configFile = mkDefault (pkgs.writeText "znc-generated.conf" semanticString); 265 config = { 266 Version = lib.getVersion pkgs.znc; 267 Listener.l.Port = mkDefault 5000; 268 Listener.l.SSL = mkDefault true; 269 }; 270 }; 271 272 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall listenerPorts; 273 274 systemd.services.znc = { 275 description = "ZNC Server"; 276 wantedBy = [ "multi-user.target" ]; 277 wants = [ "network-online.target" ]; 278 after = [ "network-online.target" ]; 279 serviceConfig = { 280 User = cfg.user; 281 Group = cfg.group; 282 Restart = "always"; 283 ExecStart = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${escapeShellArgs cfg.extraFlags}"; 284 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 285 ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; 286 # Hardening 287 CapabilityBoundingSet = [ "" ]; 288 DevicePolicy = "closed"; 289 LockPersonality = true; 290 MemoryDenyWriteExecute = true; 291 NoNewPrivileges = true; 292 PrivateDevices = true; 293 PrivateTmp = true; 294 PrivateUsers = true; 295 ProcSubset = "pid"; 296 ProtectClock = true; 297 ProtectControlGroups = true; 298 ProtectHome = true; 299 ProtectHostname = true; 300 ProtectKernelLogs = true; 301 ProtectKernelModules = true; 302 ProtectKernelTunables = true; 303 ProtectProc = "invisible"; 304 ProtectSystem = "strict"; 305 ReadWritePaths = [ cfg.dataDir ]; 306 RemoveIPC = true; 307 RestrictAddressFamilies = [ 308 "AF_INET" 309 "AF_INET6" 310 ]; 311 RestrictNamespaces = true; 312 RestrictRealtime = true; 313 RestrictSUIDSGID = true; 314 SystemCallArchitectures = "native"; 315 SystemCallFilter = [ 316 "@system-service" 317 "~@privileged" 318 "~@resources" 319 ]; 320 UMask = "0027"; 321 }; 322 preStart = '' 323 mkdir -p ${cfg.dataDir}/configs 324 325 # If mutable, regenerate conf file every time. 326 ${optionalString (!cfg.mutable) '' 327 echo "znc is set to be system-managed. Now deleting old znc.conf file to be regenerated." 328 rm -f ${cfg.dataDir}/configs/znc.conf 329 ''} 330 331 # Ensure essential files exist. 332 if [[ ! -f ${cfg.dataDir}/configs/znc.conf ]]; then 333 echo "No znc.conf file found in ${cfg.dataDir}. Creating one now." 334 cp --no-preserve=ownership --no-clobber ${cfg.configFile} ${cfg.dataDir}/configs/znc.conf 335 chmod u+rw ${cfg.dataDir}/configs/znc.conf 336 fi 337 338 if [[ ! -f ${cfg.dataDir}/znc.pem ]]; then 339 echo "No znc.pem file found in ${cfg.dataDir}. Creating one now." 340 ${pkgs.znc}/bin/znc --makepem --datadir ${cfg.dataDir} 341 fi 342 343 # Symlink modules 344 rm ${cfg.dataDir}/modules || true 345 ln -fs ${modules}/lib/znc ${cfg.dataDir}/modules 346 ''; 347 }; 348 349 users.users = optionalAttrs (cfg.user == defaultUser) { 350 ${defaultUser} = { 351 description = "ZNC server daemon owner"; 352 group = defaultUser; 353 uid = config.ids.uids.znc; 354 home = cfg.dataDir; 355 createHome = true; 356 }; 357 }; 358 359 users.groups = optionalAttrs (cfg.user == defaultUser) { 360 ${defaultUser} = { 361 gid = config.ids.gids.znc; 362 members = [ defaultUser ]; 363 }; 364 }; 365 366 }; 367}