at 21.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 "ZNC"; 85 86 user = mkOption { 87 default = "znc"; 88 example = "john"; 89 type = types.str; 90 description = '' 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 = '' 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 = '' 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 = '' 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</option>. 122 ''; 123 }; 124 125 config = mkOption { 126 type = semanticTypes.zncConf; 127 default = {}; 128 example = literalExample '' 129 { 130 LoadModule = [ "webadmin" "adminlog" ]; 131 User.paul = { 132 Admin = true; 133 Nick = "paul"; 134 AltNick = "paul1"; 135 LoadModule = [ "chansaver" "controlpanel" ]; 136 Network.freenode = { 137 Server = "chat.freenode.net +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 = '' 153 Configuration for ZNC, see 154 <link xlink:href="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.*</option>, but also can't do 158 any type checking. 159 </para> 160 <para> 161 You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.services.znc.config</command> 162 to view the current value. By default it contains a listener for port 163 5000 with SSL enabled. 164 </para> 165 <para> 166 Nix attributes called <literal>extraConfig</literal> will be inserted 167 verbatim into the resulting config file. 168 </para> 169 <para> 170 If <option>services.znc.useLegacyConfig</option> is turned on, the 171 option values in <option>services.znc.confOptions.*</option> will be 172 gracefully be applied to this option. 173 </para> 174 <para> 175 If you intend to update the configuration through this option, be sure 176 to enable <option>services.znc.mutable</option>, otherwise none of the 177 changes here will be applied after the initial deploy. 178 ''; 179 }; 180 181 configFile = mkOption { 182 type = types.path; 183 example = "~/.znc/configs/znc.conf"; 184 description = '' 185 Configuration file for ZNC. It is recommended to use the 186 <option>config</option> option instead. 187 </para> 188 <para> 189 Setting this option will override any auto-generated config file 190 through the <option>confOptions</option> or <option>config</option> 191 options. 192 ''; 193 }; 194 195 modulePackages = mkOption { 196 type = types.listOf types.package; 197 default = [ ]; 198 example = literalExample "[ pkgs.zncModules.fish pkgs.zncModules.push ]"; 199 description = '' 200 A list of global znc module packages to add to znc. 201 ''; 202 }; 203 204 mutable = mkOption { 205 default = true; # TODO: Default to true when config is set, make sure to not delete the old config if present 206 type = types.bool; 207 description = '' 208 Indicates whether to allow the contents of the 209 <literal>dataDir</literal> directory to be changed by the user at 210 run-time. 211 </para> 212 <para> 213 If enabled, modifications to the ZNC configuration after its initial 214 creation are not overwritten by a NixOS rebuild. If disabled, the 215 ZNC configuration is rebuilt on every NixOS rebuild. 216 </para> 217 <para> 218 If the user wants to manage the ZNC service using the web admin 219 interface, this option should be enabled. 220 ''; 221 }; 222 223 extraFlags = mkOption { 224 default = [ ]; 225 example = [ "--debug" ]; 226 type = types.listOf types.str; 227 description = '' 228 Extra arguments to use for executing znc. 229 ''; 230 }; 231 }; 232 }; 233 234 235 ###### Implementation 236 237 config = mkIf cfg.enable { 238 239 services.znc = { 240 configFile = mkDefault (pkgs.writeText "znc-generated.conf" semanticString); 241 config = { 242 Version = lib.getVersion pkgs.znc; 243 Listener.l.Port = mkDefault 5000; 244 Listener.l.SSL = mkDefault true; 245 }; 246 }; 247 248 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall listenerPorts; 249 250 systemd.services.znc = { 251 description = "ZNC Server"; 252 wantedBy = [ "multi-user.target" ]; 253 after = [ "network-online.target" ]; 254 serviceConfig = { 255 User = cfg.user; 256 Group = cfg.group; 257 Restart = "always"; 258 ExecStart = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${escapeShellArgs cfg.extraFlags}"; 259 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 260 ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; 261 # Hardening 262 CapabilityBoundingSet = [ "" ]; 263 DevicePolicy = "closed"; 264 LockPersonality = true; 265 MemoryDenyWriteExecute = true; 266 NoNewPrivileges = true; 267 PrivateDevices = true; 268 PrivateTmp = true; 269 PrivateUsers = true; 270 ProcSubset = "pid"; 271 ProtectClock = true; 272 ProtectControlGroups = true; 273 ProtectHome = true; 274 ProtectHostname = true; 275 ProtectKernelLogs = true; 276 ProtectKernelModules = true; 277 ProtectKernelTunables = true; 278 ProtectProc = "invisible"; 279 ProtectSystem = "strict"; 280 ReadWritePaths = [ cfg.dataDir ]; 281 RemoveIPC = true; 282 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; 283 RestrictNamespaces = true; 284 RestrictRealtime = true; 285 RestrictSUIDSGID = true; 286 SystemCallArchitectures = "native"; 287 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; 288 UMask = "0027"; 289 }; 290 preStart = '' 291 mkdir -p ${cfg.dataDir}/configs 292 293 # If mutable, regenerate conf file every time. 294 ${optionalString (!cfg.mutable) '' 295 echo "znc is set to be system-managed. Now deleting old znc.conf file to be regenerated." 296 rm -f ${cfg.dataDir}/configs/znc.conf 297 ''} 298 299 # Ensure essential files exist. 300 if [[ ! -f ${cfg.dataDir}/configs/znc.conf ]]; then 301 echo "No znc.conf file found in ${cfg.dataDir}. Creating one now." 302 cp --no-preserve=ownership --no-clobber ${cfg.configFile} ${cfg.dataDir}/configs/znc.conf 303 chmod u+rw ${cfg.dataDir}/configs/znc.conf 304 fi 305 306 if [[ ! -f ${cfg.dataDir}/znc.pem ]]; then 307 echo "No znc.pem file found in ${cfg.dataDir}. Creating one now." 308 ${pkgs.znc}/bin/znc --makepem --datadir ${cfg.dataDir} 309 fi 310 311 # Symlink modules 312 rm ${cfg.dataDir}/modules || true 313 ln -fs ${modules}/lib/znc ${cfg.dataDir}/modules 314 ''; 315 }; 316 317 users.users = optionalAttrs (cfg.user == defaultUser) { 318 ${defaultUser} = 319 { description = "ZNC server daemon owner"; 320 group = defaultUser; 321 uid = config.ids.uids.znc; 322 home = cfg.dataDir; 323 createHome = true; 324 }; 325 }; 326 327 users.groups = optionalAttrs (cfg.user == defaultUser) { 328 ${defaultUser} = 329 { gid = config.ids.gids.znc; 330 members = [ defaultUser ]; 331 }; 332 }; 333 334 }; 335}