1{ config, lib, pkgs, utils, ... }:
2with lib;
3let
4 cfg = config.services.kubo;
5
6 settingsFormat = pkgs.formats.json {};
7
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 '');
15
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" ];
20
21 customizedConfig = lib.recursiveUpdate defaultConfig cfg.settings;
22
23 configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
24
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 '';
36
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 );
45
46 profile =
47 if cfg.localDiscovery
48 then "local-discovery"
49 else "server";
50
51 splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
52
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;
60
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;
68
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
81
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
92
93in
94{
95
96 ###### interface
97
98 options = {
99
100 services.kubo = {
101
102 enable = mkEnableOption (lib.mdDoc "Interplanetary File System (WARNING: may cause severe network degradation)");
103
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 };
110
111 user = mkOption {
112 type = types.str;
113 default = "ipfs";
114 description = lib.mdDoc "User under which the Kubo daemon runs";
115 };
116
117 group = mkOption {
118 type = types.str;
119 default = "ipfs";
120 description = lib.mdDoc "Group under which the Kubo daemon runs";
121 };
122
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 };
136
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 };
142
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 };
148
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 };
154
155 ipfsMountDir = mkOption {
156 type = types.str;
157 default = "/ipfs";
158 description = lib.mdDoc "Where to mount the IPFS namespace to";
159 };
160
161 ipnsMountDir = mkOption {
162 type = types.str;
163 default = "/ipns";
164 description = lib.mdDoc "Where to mount the IPNS namespace to";
165 };
166
167 enableGC = mkOption {
168 type = types.bool;
169 default = false;
170 description = lib.mdDoc "Whether to enable automatic garbage collection";
171 };
172
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 };
178
179 settings = mkOption {
180 type = lib.types.submodule {
181 freeformType = settingsFormat.type;
182
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 };
194
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 };
200
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 };
230
231 };
232
233 extraFlags = mkOption {
234 type = types.listOf types.str;
235 description = lib.mdDoc "Extra flags passed to the Kubo daemon";
236 default = [ ];
237 };
238
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 };
246
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 };
253
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 };
259
260 };
261 };
262
263 ###### implementation
264
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 ];
286
287 environment.systemPackages = [ cfg.package ];
288 environment.variables.IPFS_PATH = fakeKuboRepo;
289
290 # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
291 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
292
293 programs.fuse = mkIf cfg.autoMount {
294 userAllowOther = true;
295 };
296
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 };
309
310 users.groups = mkIf (cfg.group == "ipfs") {
311 ipfs.gid = config.ids.gids.ipfs;
312 };
313
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 ];
320
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 ];
325
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 };
331
332 systemd.services.ipfs = {
333 path = [ "/run/wrappers" cfg.package ];
334 environment.IPFS_PATH = cfg.dataDir;
335
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}' |
348
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 };
373
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 };
383
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 };
397
398 meta = {
399 maintainers = with lib.maintainers; [ Luflosi ];
400 };
401
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 ];
428}