nixos/preload: init
[NixPkgs.git] / nixos / modules / services / network-filesystems / kubo.nix
blobbc746bed31f223bb02850c005b503aabe4ec2754
1 { config, lib, pkgs, utils, ... }:
2 with lib;
3 let
4   cfg = config.services.kubo;
6   settingsFormat = pkgs.formats.json {};
8   rawDefaultConfig = lib.importJSON (pkgs.runCommand "kubo-default-config" {
9     nativeBuildInputs = [ cfg.package ];
10   } ''
11     export IPFS_PATH="$TMPDIR"
12     ipfs init --empty-repo --profile=${profile}
13     ipfs --offline config show > "$out"
14   '');
16   # Remove the PeerID (an attribute of "Identity") of the temporary Kubo repo.
17   # The "Pinning" section contains the "RemoteServices" section, which would prevent
18   # the daemon from starting as that setting can't be changed via ipfs config replace.
19   defaultConfig = builtins.removeAttrs rawDefaultConfig [ "Identity" "Pinning" ];
21   customizedConfig = lib.recursiveUpdate defaultConfig cfg.settings;
23   configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
25   # Create a fake repo containing only the file "api".
26   # $IPFS_PATH will point to this directory instead of the real one.
27   # For some reason the Kubo CLI tools insist on reading the
28   # config file when it exists. But the Kubo daemon sets the file
29   # permissions such that only the ipfs user is allowed to read
30   # this file. This prevents normal users from talking to the daemon.
31   # To work around this terrible design, create a fake repo with no
32   # config file, only an api file and everything should work as expected.
33   fakeKuboRepo = pkgs.writeTextDir "api" ''
34     /unix/run/ipfs.sock
35   '';
37   kuboFlags = utils.escapeSystemdExecArgs (
38     optional cfg.autoMount "--mount" ++
39     optional cfg.enableGC "--enable-gc" ++
40     optional (cfg.serviceFdlimit != null) "--manage-fdlimit=false" ++
41     optional (cfg.defaultMode == "offline") "--offline" ++
42     optional (cfg.defaultMode == "norouting") "--routing=none" ++
43     cfg.extraFlags
44   );
46   profile =
47     if cfg.localDiscovery
48     then "local-discovery"
49     else "server";
51   splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
53   multiaddrsToListenStreams = addrIn:
54     let
55       addrs = if builtins.typeOf addrIn == "list"
56       then addrIn else [ addrIn ];
57       unfilteredResult = map multiaddrToListenStream addrs;
58     in
59       builtins.filter (addr: addr != null) unfilteredResult;
61   multiaddrsToListenDatagrams = addrIn:
62     let
63       addrs = if builtins.typeOf addrIn == "list"
64       then addrIn else [ addrIn ];
65       unfilteredResult = map multiaddrToListenDatagram addrs;
66     in
67       builtins.filter (addr: addr != null) unfilteredResult;
69   multiaddrToListenStream = addrRaw:
70     let
71       addr = splitMulitaddr addrRaw;
72       s = builtins.elemAt addr;
73     in
74     if s 0 == "ip4" && s 2 == "tcp"
75     then "${s 1}:${s 3}"
76     else if s 0 == "ip6" && s 2 == "tcp"
77     then "[${s 1}]:${s 3}"
78     else if s 0 == "unix"
79     then "/${lib.concatStringsSep "/" (lib.tail addr)}"
80     else null; # not valid for listen stream, skip
82   multiaddrToListenDatagram = addrRaw:
83     let
84       addr = splitMulitaddr addrRaw;
85       s = builtins.elemAt addr;
86     in
87     if s 0 == "ip4" && s 2 == "udp"
88     then "${s 1}:${s 3}"
89     else if s 0 == "ip6" && s 2 == "udp"
90     then "[${s 1}]:${s 3}"
91     else null; # not valid for listen datagram, skip
96   ###### interface
98   options = {
100     services.kubo = {
102       enable = mkEnableOption (lib.mdDoc "Interplanetary File System (WARNING: may cause severe network degradation)");
104       package = mkOption {
105         type = types.package;
106         default = pkgs.kubo;
107         defaultText = literalExpression "pkgs.kubo";
108         description = lib.mdDoc "Which Kubo package to use.";
109       };
111       user = mkOption {
112         type = types.str;
113         default = "ipfs";
114         description = lib.mdDoc "User under which the Kubo daemon runs";
115       };
117       group = mkOption {
118         type = types.str;
119         default = "ipfs";
120         description = lib.mdDoc "Group under which the Kubo daemon runs";
121       };
123       dataDir = mkOption {
124         type = types.str;
125         default =
126           if versionAtLeast config.system.stateVersion "17.09"
127           then "/var/lib/ipfs"
128           else "/var/lib/ipfs/.ipfs";
129         defaultText = literalExpression ''
130           if versionAtLeast config.system.stateVersion "17.09"
131           then "/var/lib/ipfs"
132           else "/var/lib/ipfs/.ipfs"
133         '';
134         description = lib.mdDoc "The data dir for Kubo";
135       };
137       defaultMode = mkOption {
138         type = types.enum [ "online" "offline" "norouting" ];
139         default = "online";
140         description = lib.mdDoc "systemd service that is enabled by default";
141       };
143       autoMount = mkOption {
144         type = types.bool;
145         default = false;
146         description = lib.mdDoc "Whether Kubo should try to mount /ipfs and /ipns at startup.";
147       };
149       autoMigrate = mkOption {
150         type = types.bool;
151         default = true;
152         description = lib.mdDoc "Whether Kubo should try to run the fs-repo-migration at startup.";
153       };
155       ipfsMountDir = mkOption {
156         type = types.str;
157         default = "/ipfs";
158         description = lib.mdDoc "Where to mount the IPFS namespace to";
159       };
161       ipnsMountDir = mkOption {
162         type = types.str;
163         default = "/ipns";
164         description = lib.mdDoc "Where to mount the IPNS namespace to";
165       };
167       enableGC = mkOption {
168         type = types.bool;
169         default = false;
170         description = lib.mdDoc "Whether to enable automatic garbage collection";
171       };
173       emptyRepo = mkOption {
174         type = types.bool;
175         default = true;
176         description = lib.mdDoc "If set to false, the repo will be initialized with help files";
177       };
179       settings = mkOption {
180         type = lib.types.submodule {
181           freeformType = settingsFormat.type;
183           options = {
184             Addresses.API = mkOption {
185               type = types.oneOf [ types.str (types.listOf types.str) ];
186               default = [ ];
187               description = lib.mdDoc ''
188                 Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on.
189                 In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket.
190                 To allow the ipfs CLI tools to communicate with the daemon over that socket,
191                 add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];`
192               '';
193             };
195             Addresses.Gateway = mkOption {
196               type = types.oneOf [ types.str (types.listOf types.str) ];
197               default = "/ip4/127.0.0.1/tcp/8080";
198               description = lib.mdDoc "Where the IPFS Gateway can be reached";
199             };
201             Addresses.Swarm = mkOption {
202               type = types.listOf types.str;
203               default = [
204                 "/ip4/0.0.0.0/tcp/4001"
205                 "/ip6/::/tcp/4001"
206                 "/ip4/0.0.0.0/udp/4001/quic-v1"
207                 "/ip4/0.0.0.0/udp/4001/quic-v1/webtransport"
208                 "/ip6/::/udp/4001/quic-v1"
209                 "/ip6/::/udp/4001/quic-v1/webtransport"
210               ];
211               description = lib.mdDoc "Where Kubo listens for incoming p2p connections";
212             };
213           };
214         };
215         description = lib.mdDoc ''
216           Attrset of daemon configuration.
217           See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
218           You can't set `Identity` or `Pinning`.
219         '';
220         default = { };
221         example = {
222           Datastore.StorageMax = "100GB";
223           Discovery.MDNS.Enabled = false;
224           Bootstrap = [
225             "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu"
226             "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm"
227           ];
228           Swarm.AddrFilters = null;
229         };
231       };
233       extraFlags = mkOption {
234         type = types.listOf types.str;
235         description = lib.mdDoc "Extra flags passed to the Kubo daemon";
236         default = [ ];
237       };
239       localDiscovery = mkOption {
240         type = types.bool;
241         description = lib.mdDoc ''Whether to enable local discovery for the Kubo daemon.
242           This will allow Kubo to scan ports on your local network. Some hosting services will ban you if you do this.
243         '';
244         default = false;
245       };
247       serviceFdlimit = mkOption {
248         type = types.nullOr types.int;
249         default = null;
250         description = lib.mdDoc "The fdlimit for the Kubo systemd unit or `null` to have the daemon attempt to manage it";
251         example = 64 * 1024;
252       };
254       startWhenNeeded = mkOption {
255         type = types.bool;
256         default = false;
257         description = lib.mdDoc "Whether to use socket activation to start Kubo when needed.";
258       };
260     };
261   };
263   ###### implementation
265   config = mkIf cfg.enable {
266     assertions = [
267       {
268         assertion = !builtins.hasAttr "Identity" cfg.settings;
269         message = ''
270           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.
271         '';
272       }
273       {
274         assertion = !((builtins.hasAttr "Pinning" cfg.settings) && (builtins.hasAttr "RemoteServices" cfg.settings.Pinning));
275         message = ''
276           You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
277         '';
278       }
279       {
280         assertion = !((lib.versionAtLeast cfg.package.version "0.21") && (builtins.hasAttr "Experimental" cfg.settings) && (builtins.hasAttr "AcceleratedDHTClient" cfg.settings.Experimental));
281         message = ''
282     The `services.kubo.settings.Experimental.AcceleratedDHTClient` option was renamed to `services.kubo.settings.Routing.AcceleratedDHTClient` in Kubo 0.21.
283   '';
284       }
285     ];
287     environment.systemPackages = [ cfg.package ];
288     environment.variables.IPFS_PATH = fakeKuboRepo;
290     # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
291     boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
293     programs.fuse = mkIf cfg.autoMount {
294       userAllowOther = true;
295     };
297     users.users = mkIf (cfg.user == "ipfs") {
298       ipfs = {
299         group = cfg.group;
300         home = cfg.dataDir;
301         createHome = false;
302         uid = config.ids.uids.ipfs;
303         description = "IPFS daemon user";
304         packages = [
305           pkgs.kubo-migrator
306         ];
307       };
308     };
310     users.groups = mkIf (cfg.group == "ipfs") {
311       ipfs.gid = config.ids.gids.ipfs;
312     };
314     systemd.tmpfiles.rules = [
315       "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
316     ] ++ optionals cfg.autoMount [
317       "d '${cfg.ipfsMountDir}' - ${cfg.user} ${cfg.group} - -"
318       "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
319     ];
321     # The hardened systemd unit breaks the fuse-mount function according to documentation in the unit file itself
322     systemd.packages = if cfg.autoMount
323       then [ cfg.package.systemd_unit ]
324       else [ cfg.package.systemd_unit_hardened ];
326     services.kubo.settings = mkIf cfg.autoMount {
327       Mounts.FuseAllowOther = lib.mkDefault true;
328       Mounts.IPFS = lib.mkDefault cfg.ipfsMountDir;
329       Mounts.IPNS = lib.mkDefault cfg.ipnsMountDir;
330     };
332     systemd.services.ipfs = {
333       path = [ "/run/wrappers" cfg.package ];
334       environment.IPFS_PATH = cfg.dataDir;
336       preStart = ''
337         if [[ ! -f "$IPFS_PATH/config" ]]; then
338           ipfs init --empty-repo=${lib.boolToString cfg.emptyRepo}
339         else
340           # 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.
341           rm -vf "$IPFS_PATH/api"
342       '' + optionalString cfg.autoMigrate ''
343         ${pkgs.kubo-migrator}/bin/fs-repo-migrations -to '${cfg.package.repoVersion}' -y
344       '' + ''
345         fi
346         ipfs --offline config show |
347           ${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' |
349           # This command automatically injects the private key and other secrets from
350           # the old config file back into the new config file.
351           # Unfortunately, it doesn't keep the original `Identity.PeerID`,
352           # so we need `ipfs config show` and jq above.
353           # See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem.
354           # Kubo also wants a specific version of the original "Pinning.RemoteServices"
355           # section (redacted by `ipfs config show`), such that that section doesn't
356           # change when the changes are applied. Whyyyyyy.....
357           ipfs --offline config replace -
358       '';
359       postStop = mkIf cfg.autoMount ''
360         # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked
361         umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true
362       '';
363       serviceConfig = {
364         ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ];
365         User = cfg.user;
366         Group = cfg.group;
367         StateDirectory = "";
368         ReadWritePaths = optionals (!cfg.autoMount) [ "" cfg.dataDir ];
369       } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
370     } // optionalAttrs (!cfg.startWhenNeeded) {
371       wantedBy = [ "default.target" ];
372     };
374     systemd.sockets.ipfs-gateway = {
375       wantedBy = [ "sockets.target" ];
376       socketConfig = {
377         ListenStream =
378           [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
379         ListenDatagram =
380           [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
381       };
382     };
384     systemd.sockets.ipfs-api = {
385       wantedBy = [ "sockets.target" ];
386       socketConfig = {
387         # We also include "%t/ipfs.sock" because there is no way to put the "%t"
388         # in the multiaddr.
389         ListenStream =
390           [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
391         SocketMode = "0660";
392         SocketUser = cfg.user;
393         SocketGroup = cfg.group;
394       };
395     };
396   };
398   meta = {
399     maintainers = with lib.maintainers; [ Luflosi ];
400   };
402   imports = [
403     (mkRenamedOptionModule [ "services" "ipfs" "enable" ] [ "services" "kubo" "enable" ])
404     (mkRenamedOptionModule [ "services" "ipfs" "package" ] [ "services" "kubo" "package" ])
405     (mkRenamedOptionModule [ "services" "ipfs" "user" ] [ "services" "kubo" "user" ])
406     (mkRenamedOptionModule [ "services" "ipfs" "group" ] [ "services" "kubo" "group" ])
407     (mkRenamedOptionModule [ "services" "ipfs" "dataDir" ] [ "services" "kubo" "dataDir" ])
408     (mkRenamedOptionModule [ "services" "ipfs" "defaultMode" ] [ "services" "kubo" "defaultMode" ])
409     (mkRenamedOptionModule [ "services" "ipfs" "autoMount" ] [ "services" "kubo" "autoMount" ])
410     (mkRenamedOptionModule [ "services" "ipfs" "autoMigrate" ] [ "services" "kubo" "autoMigrate" ])
411     (mkRenamedOptionModule [ "services" "ipfs" "ipfsMountDir" ] [ "services" "kubo" "ipfsMountDir" ])
412     (mkRenamedOptionModule [ "services" "ipfs" "ipnsMountDir" ] [ "services" "kubo" "ipnsMountDir" ])
413     (mkRenamedOptionModule [ "services" "ipfs" "gatewayAddress" ] [ "services" "kubo" "settings" "Addresses" "Gateway" ])
414     (mkRenamedOptionModule [ "services" "ipfs" "apiAddress" ] [ "services" "kubo" "settings" "Addresses" "API" ])
415     (mkRenamedOptionModule [ "services" "ipfs" "swarmAddress" ] [ "services" "kubo" "settings" "Addresses" "Swarm" ])
416     (mkRenamedOptionModule [ "services" "ipfs" "enableGC" ] [ "services" "kubo" "enableGC" ])
417     (mkRenamedOptionModule [ "services" "ipfs" "emptyRepo" ] [ "services" "kubo" "emptyRepo" ])
418     (mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "settings" ])
419     (mkRenamedOptionModule [ "services" "ipfs" "extraFlags" ] [ "services" "kubo" "extraFlags" ])
420     (mkRenamedOptionModule [ "services" "ipfs" "localDiscovery" ] [ "services" "kubo" "localDiscovery" ])
421     (mkRenamedOptionModule [ "services" "ipfs" "serviceFdlimit" ] [ "services" "kubo" "serviceFdlimit" ])
422     (mkRenamedOptionModule [ "services" "ipfs" "startWhenNeeded" ] [ "services" "kubo" "startWhenNeeded" ])
423     (mkRenamedOptionModule [ "services" "kubo" "extraConfig" ] [ "services" "kubo" "settings" ])
424     (mkRenamedOptionModule [ "services" "kubo" "gatewayAddress" ] [ "services" "kubo" "settings" "Addresses" "Gateway" ])
425     (mkRenamedOptionModule [ "services" "kubo" "apiAddress" ] [ "services" "kubo" "settings" "Addresses" "API" ])
426     (mkRenamedOptionModule [ "services" "kubo" "swarmAddress" ] [ "services" "kubo" "settings" "Addresses" "Swarm" ])
427   ];