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 '<nixpkgs/nixos>' -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}