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}