1{
2 config,
3 lib,
4 pkgs,
5 options,
6 ...
7}:
8let
9 cfg = config.services.biboumi;
10 inherit (config.environment) etc;
11 rootDir = "/run/biboumi/mnt-root";
12 stateDir = "/var/lib/biboumi";
13 settingsFile = pkgs.writeText "biboumi.cfg" (
14 lib.generators.toKeyValue {
15 mkKeyValue = k: v: lib.optionalString (v != null) (lib.generators.mkKeyValueDefault { } "=" k v);
16 } cfg.settings
17 );
18 need_CAP_NET_BIND_SERVICE = cfg.settings.identd_port != 0 && cfg.settings.identd_port < 1024;
19in
20{
21 options = {
22 services.biboumi = {
23 enable = lib.mkEnableOption "the Biboumi XMPP gateway to IRC";
24
25 package = lib.mkPackageOption pkgs "biboumi" { };
26
27 settings = lib.mkOption {
28 description = ''
29 See [biboumi 9.0](https://doc.biboumi.louiz.org/9.0/admin.html#configuration)
30
31 for documentation.
32 '';
33 default = { };
34 type = lib.types.submodule {
35 freeformType =
36 with lib.types;
37 (attrsOf (
38 nullOr (oneOf [
39 str
40 int
41 bool
42 ])
43 ))
44 // {
45 description = "settings option";
46 };
47 options.admin = lib.mkOption {
48 type = with lib.types; listOf str;
49 default = [ ];
50 example = [ "admin@example.org" ];
51 apply = lib.concatStringsSep ":";
52 description = ''
53 The bare JID of the gateway administrator. This JID will have more
54 privileges than other standard users, for example some administration
55 ad-hoc commands will only be available to that JID.
56 '';
57 };
58 options.ca_file = lib.mkOption {
59 type = lib.types.path;
60 default = config.security.pki.caBundle;
61 defaultText = lib.literalExpression "config.security.pki.caBundle";
62 description = ''
63 Specifies which file should be used as the list of trusted CA
64 when negotiating a TLS session.
65 '';
66 };
67 options.db_name = lib.mkOption {
68 type = with lib.types; nullOr (either path str);
69 default = "${stateDir}/biboumi.sqlite";
70 description = ''
71 The name of the database to use.
72
73 Set it to null and use [credentialsFile](#opt-services.biboumi.credentialsFile)
74 if you do not want this connection string to go into the Nix store.
75 '';
76 example = "postgresql://user:secret@localhost";
77 };
78 options.hostname = lib.mkOption {
79 type = lib.types.str;
80 example = "biboumi.example.org";
81 description = ''
82 The hostname served by the XMPP gateway.
83 This domain must be configured in the XMPP server
84 as an external component.
85 '';
86 };
87 options.identd_port = lib.mkOption {
88 type = lib.types.port;
89 default = 113;
90 example = 0;
91 description = ''
92 The TCP port on which to listen for identd queries.
93 '';
94 };
95 options.log_level = lib.mkOption {
96 type = lib.types.ints.between 0 3;
97 default = 1;
98 description = ''
99 Indicate what type of log messages to write in the logs.
100 0 is debug, 1 is info, 2 is warning, 3 is error.
101 '';
102 };
103 options.password = lib.mkOption {
104 type = with lib.types; nullOr str;
105 description = ''
106 The password used to authenticate the XMPP component to your XMPP server.
107 This password must be configured in the XMPP server,
108 associated with the external component on
109 [hostname](#opt-services.biboumi.settings.hostname).
110
111 Set it to null and use [credentialsFile](#opt-services.biboumi.credentialsFile)
112 if you do not want this password to go into the Nix store.
113 '';
114 };
115 options.persistent_by_default = lib.mkOption {
116 type = lib.types.bool;
117 default = false;
118 description = ''
119 Whether all rooms will be persistent by default:
120 the value of the “persistent” option in the global configuration of each
121 user will be “true”, but the value of each individual room will still
122 default to false. This means that a user just needs to change the global
123 “persistent” configuration option to false in order to override this.
124 '';
125 };
126 options.policy_directory = lib.mkOption {
127 type = lib.types.path;
128 default = "${cfg.package}/etc/biboumi";
129 defaultText = lib.literalExpression ''"''${pkgs.biboumi}/etc/biboumi"'';
130 description = ''
131 A directory that should contain the policy files,
132 used to customize Botan’s behaviour
133 when negotiating the TLS connections with the IRC servers.
134 '';
135 };
136 options.port = lib.mkOption {
137 type = lib.types.port;
138 default = 5347;
139 description = ''
140 The TCP port to use to connect to the local XMPP component.
141 '';
142 };
143 options.realname_customization = lib.mkOption {
144 type = lib.types.bool;
145 default = true;
146 description = ''
147 Whether the users will be able to use
148 the ad-hoc commands that lets them configure
149 their realname and username.
150 '';
151 };
152 options.realname_from_jid = lib.mkOption {
153 type = lib.types.bool;
154 default = false;
155 description = ''
156 Whether the realname and username of each biboumi
157 user will be extracted from their JID.
158 Otherwise they will be set to the nick
159 they used to connect to the IRC server.
160 '';
161 };
162 options.xmpp_server_ip = lib.mkOption {
163 type = lib.types.str;
164 default = "127.0.0.1";
165 description = ''
166 The IP address to connect to the XMPP server on.
167 The connection to the XMPP server is unencrypted,
168 so the biboumi instance and the server should
169 normally be on the same host.
170 '';
171 };
172 };
173 };
174
175 credentialsFile = lib.mkOption {
176 type = lib.types.path;
177 description = ''
178 Path to a configuration file to be merged with the settings.
179 Beware not to surround "=" with spaces when setting biboumi's options in this file.
180 Useful to merge a file which is better kept out of the Nix store
181 because it contains sensible data like
182 [password](#opt-services.biboumi.settings.password).
183 '';
184 default = "/dev/null";
185 example = "/run/keys/biboumi.cfg";
186 };
187
188 openFirewall = lib.mkEnableOption "opening of the identd port in the firewall";
189 };
190 };
191
192 config = lib.mkIf cfg.enable {
193 networking.firewall = lib.mkIf (cfg.openFirewall && cfg.settings.identd_port != 0) {
194 allowedTCPPorts = [ cfg.settings.identd_port ];
195 };
196
197 systemd.services.biboumi = {
198 description = "Biboumi, XMPP to IRC gateway";
199 after = [ "network.target" ];
200 wantedBy = [ "multi-user.target" ];
201
202 serviceConfig = {
203 Type = "notify";
204 # Biboumi supports systemd's watchdog.
205 WatchdogSec = 20;
206 Restart = "always";
207 # Use "+" because credentialsFile may not be accessible to User= or Group=.
208 ExecStartPre = [
209 (
210 "+"
211 + pkgs.writeShellScript "biboumi-prestart" ''
212 set -eux
213 cat ${settingsFile} '${cfg.credentialsFile}' |
214 install -m 644 /dev/stdin /run/biboumi/biboumi.cfg
215 ''
216 )
217 ];
218 ExecStart = "${lib.getExe cfg.package} /run/biboumi/biboumi.cfg";
219 ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
220 # Firewalls needing opening for output connections can still do that
221 # selectively for biboumi with:
222 # users.users.biboumi.isSystemUser = true;
223 # and, for example:
224 # networking.nftables.ruleset = ''
225 # add rule inet filter output meta skuid biboumi tcp accept
226 # '';
227 DynamicUser = true;
228 RootDirectory = rootDir;
229 RootDirectoryStartOnly = true;
230 InaccessiblePaths = [ "-+${rootDir}" ];
231 RuntimeDirectory = [
232 "biboumi"
233 (lib.removePrefix "/run/" rootDir)
234 ];
235 RuntimeDirectoryMode = "700";
236 StateDirectory = "biboumi";
237 StateDirectoryMode = "700";
238 MountAPIVFS = true;
239 UMask = "0066";
240 BindPaths = [
241 stateDir
242 # This is for Type="notify"
243 # See https://github.com/systemd/systemd/issues/3544
244 "/run/systemd/notify"
245 "/run/systemd/journal/socket"
246 ];
247 BindReadOnlyPaths = [
248 builtins.storeDir
249 "/etc"
250 ];
251 # The following options are only for optimizing:
252 # systemd-analyze security biboumi
253 AmbientCapabilities = [ (lib.optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
254 CapabilityBoundingSet = [ (lib.optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
255 # ProtectClock= adds DeviceAllow=char-rtc r
256 DeviceAllow = "";
257 LockPersonality = true;
258 MemoryDenyWriteExecute = true;
259 NoNewPrivileges = true;
260 PrivateDevices = true;
261 PrivateMounts = true;
262 PrivateNetwork = lib.mkDefault false;
263 PrivateTmp = true;
264 # PrivateUsers=true breaks AmbientCapabilities=CAP_NET_BIND_SERVICE
265 # See https://bugs.archlinux.org/task/65921
266 PrivateUsers = !need_CAP_NET_BIND_SERVICE;
267 ProtectClock = true;
268 ProtectControlGroups = true;
269 ProtectHome = true;
270 ProtectHostname = true;
271 ProtectKernelLogs = true;
272 ProtectKernelModules = true;
273 ProtectKernelTunables = true;
274 ProtectSystem = "strict";
275 RemoveIPC = true;
276 # AF_UNIX is for /run/systemd/notify
277 RestrictAddressFamilies = [
278 "AF_UNIX"
279 "AF_INET"
280 "AF_INET6"
281 ];
282 RestrictNamespaces = true;
283 RestrictRealtime = true;
284 RestrictSUIDSGID = true;
285 SystemCallFilter = [
286 "@system-service"
287 # Groups in @system-service which do not contain a syscall
288 # listed by perf stat -e 'syscalls:sys_enter_*' biboumi biboumi.cfg
289 # in tests, and seem likely not necessary for biboumi.
290 # To run such a perf in ExecStart=, you have to:
291 # - AmbientCapabilities="CAP_SYS_ADMIN"
292 # - mount -o remount,mode=755 /sys/kernel/debug/{,tracing}
293 "~@aio"
294 "~@chown"
295 "~@ipc"
296 "~@keyring"
297 "~@resources"
298 "~@setuid"
299 "~@timer"
300 ];
301 SystemCallArchitectures = "native";
302 SystemCallErrorNumber = "EPERM";
303 };
304 };
305 };
306
307 meta.maintainers = with lib.maintainers; [ julm ];
308}