1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.kubo;
10
11 settingsFormat = pkgs.formats.json { };
12
13 rawDefaultConfig = lib.importJSON (
14 pkgs.runCommand "kubo-default-config"
15 {
16 nativeBuildInputs = [ cfg.package ];
17 }
18 ''
19 export IPFS_PATH="$TMPDIR"
20 ipfs init --empty-repo --profile=${profile}
21 ipfs --offline config show > "$out"
22 ''
23 );
24
25 # Remove the PeerID (an attribute of "Identity") of the temporary Kubo repo.
26 # The "Pinning" section contains the "RemoteServices" section, which would prevent
27 # the daemon from starting as that setting can't be changed via ipfs config replace.
28 defaultConfig = builtins.removeAttrs rawDefaultConfig [
29 "Identity"
30 "Pinning"
31 ];
32
33 customizedConfig = lib.recursiveUpdate defaultConfig cfg.settings;
34
35 configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
36
37 # Create a fake repo containing only the file "api".
38 # $IPFS_PATH will point to this directory instead of the real one.
39 # For some reason the Kubo CLI tools insist on reading the
40 # config file when it exists. But the Kubo daemon sets the file
41 # permissions such that only the ipfs user is allowed to read
42 # this file. This prevents normal users from talking to the daemon.
43 # To work around this terrible design, create a fake repo with no
44 # config file, only an api file and everything should work as expected.
45 fakeKuboRepo = pkgs.writeTextDir "api" ''
46 /unix/run/ipfs.sock
47 '';
48
49 kuboFlags = utils.escapeSystemdExecArgs (
50 lib.optional cfg.autoMount "--mount"
51 ++ lib.optional cfg.enableGC "--enable-gc"
52 ++ lib.optional (cfg.serviceFdlimit != null) "--manage-fdlimit=false"
53 ++ lib.optional (cfg.defaultMode == "offline") "--offline"
54 ++ lib.optional (cfg.defaultMode == "norouting") "--routing=none"
55 ++ cfg.extraFlags
56 );
57
58 profile = if cfg.localDiscovery then "local-discovery" else "server";
59
60 splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
61
62 multiaddrsToListenStreams =
63 addrIn:
64 let
65 addrs = if builtins.isList addrIn then addrIn else [ addrIn ];
66 unfilteredResult = map multiaddrToListenStream addrs;
67 in
68 builtins.filter (addr: addr != null) unfilteredResult;
69
70 multiaddrsToListenDatagrams =
71 addrIn:
72 let
73 addrs = if builtins.isList addrIn then addrIn else [ addrIn ];
74 unfilteredResult = map multiaddrToListenDatagram addrs;
75 in
76 builtins.filter (addr: addr != null) unfilteredResult;
77
78 multiaddrToListenStream =
79 addrRaw:
80 let
81 addr = splitMulitaddr addrRaw;
82 s = builtins.elemAt addr;
83 in
84 if s 0 == "ip4" && s 2 == "tcp" then
85 "${s 1}:${s 3}"
86 else if s 0 == "ip6" && s 2 == "tcp" then
87 "[${s 1}]:${s 3}"
88 else if s 0 == "unix" then
89 "/${lib.concatStringsSep "/" (lib.tail addr)}"
90 else
91 null; # not valid for listen stream, skip
92
93 multiaddrToListenDatagram =
94 addrRaw:
95 let
96 addr = splitMulitaddr addrRaw;
97 s = builtins.elemAt addr;
98 in
99 if s 0 == "ip4" && s 2 == "udp" then
100 "${s 1}:${s 3}"
101 else if s 0 == "ip6" && s 2 == "udp" then
102 "[${s 1}]:${s 3}"
103 else
104 null; # not valid for listen datagram, skip
105
106in
107{
108
109 ###### interface
110
111 options = {
112
113 services.kubo = {
114
115 enable = lib.mkEnableOption ''
116 the Interplanetary File System (WARNING: may cause severe network degradation).
117 NOTE: after enabling this option and rebuilding your system, you need to log out
118 and back in for the `IPFS_PATH` environment variable to be present in your shell.
119 Until you do that, the CLI tools won't be able to talk to the daemon by default
120 '';
121
122 package = lib.mkPackageOption pkgs "kubo" { };
123
124 user = lib.mkOption {
125 type = lib.types.str;
126 default = "ipfs";
127 description = "User under which the Kubo daemon runs";
128 };
129
130 group = lib.mkOption {
131 type = lib.types.str;
132 default = "ipfs";
133 description = "Group under which the Kubo daemon runs";
134 };
135
136 dataDir = lib.mkOption {
137 type = lib.types.str;
138 default =
139 if lib.versionAtLeast config.system.stateVersion "17.09" then
140 "/var/lib/ipfs"
141 else
142 "/var/lib/ipfs/.ipfs";
143 defaultText = lib.literalExpression ''
144 if lib.versionAtLeast config.system.stateVersion "17.09"
145 then "/var/lib/ipfs"
146 else "/var/lib/ipfs/.ipfs"
147 '';
148 description = "The data dir for Kubo";
149 };
150
151 defaultMode = lib.mkOption {
152 type = lib.types.enum [
153 "online"
154 "offline"
155 "norouting"
156 ];
157 default = "online";
158 description = "systemd service that is enabled by default";
159 };
160
161 autoMount = lib.mkOption {
162 type = lib.types.bool;
163 default = false;
164 description = "Whether Kubo should try to mount /ipfs, /ipns and /mfs at startup.";
165 };
166
167 autoMigrate = lib.mkOption {
168 type = lib.types.bool;
169 default = true;
170 description = "Whether Kubo should try to run the fs-repo-migration at startup.";
171 };
172
173 enableGC = lib.mkOption {
174 type = lib.types.bool;
175 default = false;
176 description = "Whether to enable automatic garbage collection";
177 };
178
179 emptyRepo = lib.mkOption {
180 type = lib.types.bool;
181 default = true;
182 description = "If set to false, the repo will be initialized with help files";
183 };
184
185 settings = lib.mkOption {
186 type = lib.types.submodule {
187 freeformType = settingsFormat.type;
188
189 options = {
190 Addresses.API = lib.mkOption {
191 type = lib.types.oneOf [
192 lib.types.str
193 (lib.types.listOf lib.types.str)
194 ];
195 default = [ ];
196 description = ''
197 Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on.
198 In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket.
199 To allow the ipfs CLI tools to communicate with the daemon over that socket,
200 add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];`
201 '';
202 };
203
204 Addresses.Gateway = lib.mkOption {
205 type = lib.types.oneOf [
206 lib.types.str
207 (lib.types.listOf lib.types.str)
208 ];
209 default = "/ip4/127.0.0.1/tcp/8080";
210 description = "Where the IPFS Gateway can be reached";
211 };
212
213 Addresses.Swarm = lib.mkOption {
214 type = lib.types.listOf lib.types.str;
215 default = [
216 "/ip4/0.0.0.0/tcp/4001"
217 "/ip6/::/tcp/4001"
218 "/ip4/0.0.0.0/udp/4001/quic-v1"
219 "/ip4/0.0.0.0/udp/4001/quic-v1/webtransport"
220 "/ip4/0.0.0.0/udp/4001/webrtc-direct"
221 "/ip6/::/udp/4001/quic-v1"
222 "/ip6/::/udp/4001/quic-v1/webtransport"
223 "/ip6/::/udp/4001/webrtc-direct"
224 ];
225 description = "Where Kubo listens for incoming p2p connections";
226 };
227
228 Mounts.IPFS = lib.mkOption {
229 type = lib.types.str;
230 default = "/ipfs";
231 description = "Where to mount the IPFS namespace to";
232 };
233
234 Mounts.IPNS = lib.mkOption {
235 type = lib.types.str;
236 default = "/ipns";
237 description = "Where to mount the IPNS namespace to";
238 };
239
240 Mounts.MFS = lib.mkOption {
241 type = lib.types.str;
242 default = "/mfs";
243 description = "Where to mount the MFS namespace to";
244 };
245 };
246 };
247 description = ''
248 Attrset of daemon configuration.
249 See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
250 You can't set `Identity` or `Pinning`.
251 '';
252 default = { };
253 example = {
254 Datastore.StorageMax = "100GB";
255 Discovery.MDNS.Enabled = false;
256 Bootstrap = [
257 "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu"
258 "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm"
259 ];
260 Swarm.AddrFilters = null;
261 };
262
263 };
264
265 extraFlags = lib.mkOption {
266 type = lib.types.listOf lib.types.str;
267 description = "Extra flags passed to the Kubo daemon";
268 default = [ ];
269 };
270
271 localDiscovery = lib.mkOption {
272 type = lib.types.bool;
273 description = ''
274 Whether to enable local discovery for the Kubo daemon.
275 This will allow Kubo to scan ports on your local network. Some hosting services will ban you if you do this.
276 '';
277 default = false;
278 };
279
280 serviceFdlimit = lib.mkOption {
281 type = lib.types.nullOr lib.types.int;
282 default = null;
283 description = "The fdlimit for the Kubo systemd unit or `null` to have the daemon attempt to manage it";
284 example = 64 * 1024;
285 };
286
287 startWhenNeeded = lib.mkOption {
288 type = lib.types.bool;
289 default = false;
290 description = "Whether to use socket activation to start Kubo when needed.";
291 };
292
293 };
294 };
295
296 ###### implementation
297
298 config = lib.mkIf cfg.enable {
299 assertions = [
300 {
301 assertion = !builtins.hasAttr "Identity" cfg.settings;
302 message = ''
303 You can't set services.kubo.settings.Identity because the ``config replace`` subcommand used at startup does not support modifying any of the Identity settings.
304 '';
305 }
306 {
307 assertion =
308 !(
309 (builtins.hasAttr "Pinning" cfg.settings)
310 && (builtins.hasAttr "RemoteServices" cfg.settings.Pinning)
311 );
312 message = ''
313 You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
314 '';
315 }
316 {
317 assertion =
318 !(
319 (lib.versionAtLeast cfg.package.version "0.21")
320 && (builtins.hasAttr "Experimental" cfg.settings)
321 && (builtins.hasAttr "AcceleratedDHTClient" cfg.settings.Experimental)
322 );
323 message = ''
324 The `services.kubo.settings.Experimental.AcceleratedDHTClient` option was renamed to `services.kubo.settings.Routing.AcceleratedDHTClient` in Kubo 0.21.
325 '';
326 }
327 ];
328
329 environment.systemPackages = [ cfg.package ];
330 environment.variables.IPFS_PATH = fakeKuboRepo;
331
332 # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes
333 boot.kernel.sysctl."net.core.rmem_max" = lib.mkDefault 2500000;
334 boot.kernel.sysctl."net.core.wmem_max" = lib.mkDefault 2500000;
335
336 programs.fuse = lib.mkIf cfg.autoMount {
337 userAllowOther = true;
338 };
339
340 users.users = lib.mkIf (cfg.user == "ipfs") {
341 ipfs = {
342 group = cfg.group;
343 home = cfg.dataDir;
344 createHome = false;
345 uid = config.ids.uids.ipfs;
346 description = "IPFS daemon user";
347 packages = [
348 pkgs.kubo-migrator
349 ];
350 };
351 };
352
353 users.groups = lib.mkIf (cfg.group == "ipfs") {
354 ipfs.gid = config.ids.gids.ipfs;
355 };
356
357 systemd.tmpfiles.settings."10-kubo" =
358 let
359 defaultConfig = { inherit (cfg) user group; };
360 in
361 {
362 ${cfg.dataDir}.d = defaultConfig;
363 ${cfg.settings.Mounts.IPFS}.d = lib.mkIf (cfg.autoMount) defaultConfig;
364 ${cfg.settings.Mounts.IPNS}.d = lib.mkIf (cfg.autoMount) defaultConfig;
365 ${cfg.settings.Mounts.MFS}.d = lib.mkIf (cfg.autoMount) defaultConfig;
366 };
367
368 # The hardened systemd unit breaks the fuse-mount function according to documentation in the unit file itself
369 systemd.packages =
370 if cfg.autoMount then [ cfg.package.systemd_unit ] else [ cfg.package.systemd_unit_hardened ];
371
372 services.kubo.settings = lib.mkIf cfg.autoMount {
373 Mounts.FuseAllowOther = lib.mkDefault true;
374 };
375
376 systemd.services.ipfs = {
377 path = [
378 "/run/wrappers"
379 cfg.package
380 ];
381 environment.IPFS_PATH = cfg.dataDir;
382
383 preStart = ''
384 if [[ ! -f "$IPFS_PATH/config" ]]; then
385 ipfs init --empty-repo=${lib.boolToString cfg.emptyRepo}
386 else
387 # After an unclean shutdown this file may exist which will cause the config command to attempt to talk to the daemon. This will hang forever if systemd is holding our sockets open.
388 rm -vf "$IPFS_PATH/api"
389 ''
390 + lib.optionalString cfg.autoMigrate ''
391 '${lib.getExe pkgs.kubo-migrator}' -to '${cfg.package.repoVersion}' -y
392 ''
393 + ''
394 fi
395 ipfs --offline config show |
396 ${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' |
397
398 # This command automatically injects the private key and other secrets from
399 # the old config file back into the new config file.
400 # Unfortunately, it doesn't keep the original `Identity.PeerID`,
401 # so we need `ipfs config show` and jq above.
402 # See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem.
403 # Kubo also wants a specific version of the original "Pinning.RemoteServices"
404 # section (redacted by `ipfs config show`), such that that section doesn't
405 # change when the changes are applied. Whyyyyyy.....
406 ipfs --offline config replace -
407 '';
408 postStop = lib.mkIf cfg.autoMount ''
409 # After an unclean shutdown the fuse mounts at cfg.settings.Mounts.IPFS, cfg.settings.Mounts.IPNS and cfg.settings.Mounts.MFS are locked
410 umount --quiet '${cfg.settings.Mounts.IPFS}' '${cfg.settings.Mounts.IPNS}' '${cfg.settings.Mounts.MFS}' || true
411 '';
412 serviceConfig = {
413 ExecStart = [
414 ""
415 "${cfg.package}/bin/ipfs daemon ${kuboFlags}"
416 ];
417 User = cfg.user;
418 Group = cfg.group;
419 StateDirectory = "";
420 ReadWritePaths = lib.optionals (!cfg.autoMount) [
421 ""
422 cfg.dataDir
423 ];
424 # Make sure the socket units are started before ipfs.service
425 Sockets = [
426 "ipfs-gateway.socket"
427 "ipfs-api.socket"
428 ];
429 }
430 // lib.optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
431 }
432 // lib.optionalAttrs (!cfg.startWhenNeeded) {
433 wantedBy = [ "default.target" ];
434 };
435
436 systemd.sockets.ipfs-gateway = {
437 wantedBy = [ "sockets.target" ];
438 socketConfig = {
439 ListenStream = [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
440 ListenDatagram = [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
441 };
442 };
443
444 systemd.sockets.ipfs-api = {
445 wantedBy = [ "sockets.target" ];
446 socketConfig = {
447 # We also include "%t/ipfs.sock" because there is no way to put the "%t"
448 # in the multiaddr.
449 ListenStream = [
450 ""
451 "%t/ipfs.sock"
452 ]
453 ++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
454 SocketMode = "0660";
455 SocketUser = cfg.user;
456 SocketGroup = cfg.group;
457 };
458 };
459 };
460
461 meta = {
462 maintainers = with lib.maintainers; [ Luflosi ];
463 };
464
465 imports = [
466 (lib.mkRenamedOptionModule [ "services" "ipfs" "enable" ] [ "services" "kubo" "enable" ])
467 (lib.mkRenamedOptionModule [ "services" "ipfs" "package" ] [ "services" "kubo" "package" ])
468 (lib.mkRenamedOptionModule [ "services" "ipfs" "user" ] [ "services" "kubo" "user" ])
469 (lib.mkRenamedOptionModule [ "services" "ipfs" "group" ] [ "services" "kubo" "group" ])
470 (lib.mkRenamedOptionModule [ "services" "ipfs" "dataDir" ] [ "services" "kubo" "dataDir" ])
471 (lib.mkRenamedOptionModule [ "services" "ipfs" "defaultMode" ] [ "services" "kubo" "defaultMode" ])
472 (lib.mkRenamedOptionModule [ "services" "ipfs" "autoMount" ] [ "services" "kubo" "autoMount" ])
473 (lib.mkRenamedOptionModule [ "services" "ipfs" "autoMigrate" ] [ "services" "kubo" "autoMigrate" ])
474 (lib.mkRenamedOptionModule
475 [ "services" "ipfs" "ipfsMountDir" ]
476 [ "services" "kubo" "settings" "Mounts" "IPFS" ]
477 )
478 (lib.mkRenamedOptionModule
479 [ "services" "ipfs" "ipnsMountDir" ]
480 [ "services" "kubo" "settings" "Mounts" "IPNS" ]
481 )
482 (lib.mkRenamedOptionModule
483 [ "services" "ipfs" "gatewayAddress" ]
484 [ "services" "kubo" "settings" "Addresses" "Gateway" ]
485 )
486 (lib.mkRenamedOptionModule
487 [ "services" "ipfs" "apiAddress" ]
488 [ "services" "kubo" "settings" "Addresses" "API" ]
489 )
490 (lib.mkRenamedOptionModule
491 [ "services" "ipfs" "swarmAddress" ]
492 [ "services" "kubo" "settings" "Addresses" "Swarm" ]
493 )
494 (lib.mkRenamedOptionModule [ "services" "ipfs" "enableGC" ] [ "services" "kubo" "enableGC" ])
495 (lib.mkRenamedOptionModule [ "services" "ipfs" "emptyRepo" ] [ "services" "kubo" "emptyRepo" ])
496 (lib.mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "settings" ])
497 (lib.mkRenamedOptionModule [ "services" "ipfs" "extraFlags" ] [ "services" "kubo" "extraFlags" ])
498 (lib.mkRenamedOptionModule
499 [ "services" "ipfs" "localDiscovery" ]
500 [ "services" "kubo" "localDiscovery" ]
501 )
502 (lib.mkRenamedOptionModule
503 [ "services" "ipfs" "serviceFdlimit" ]
504 [ "services" "kubo" "serviceFdlimit" ]
505 )
506 (lib.mkRenamedOptionModule
507 [ "services" "ipfs" "startWhenNeeded" ]
508 [ "services" "kubo" "startWhenNeeded" ]
509 )
510 (lib.mkRenamedOptionModule [ "services" "kubo" "extraConfig" ] [ "services" "kubo" "settings" ])
511 (lib.mkRenamedOptionModule
512 [ "services" "kubo" "gatewayAddress" ]
513 [ "services" "kubo" "settings" "Addresses" "Gateway" ]
514 )
515 (lib.mkRenamedOptionModule
516 [ "services" "kubo" "apiAddress" ]
517 [ "services" "kubo" "settings" "Addresses" "API" ]
518 )
519 (lib.mkRenamedOptionModule
520 [ "services" "kubo" "swarmAddress" ]
521 [ "services" "kubo" "settings" "Addresses" "Swarm" ]
522 )
523 (lib.mkRenamedOptionModule
524 [ "services" "kubo" "ipfsMountDir" ]
525 [ "services" "kubo" "settings" "Mounts" "IPFS" ]
526 )
527 (lib.mkRenamedOptionModule
528 [ "services" "kubo" "ipnsMountDir" ]
529 [ "services" "kubo" "settings" "Mounts" "IPNS" ]
530 )
531 ];
532}