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}