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