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"
207 "/ip4/0.0.0.0/udp/4001/quic-v1"
208 "/ip4/0.0.0.0/udp/4001/quic-v1/webtransport"
209 "/ip6/::/udp/4001/quic"
210 "/ip6/::/udp/4001/quic-v1"
211 "/ip6/::/udp/4001/quic-v1/webtransport"
212 ];
213 description = lib.mdDoc "Where Kubo listens for incoming p2p connections";
214 };
215 };
216 };
217 description = lib.mdDoc ''
218 Attrset of daemon configuration.
219 See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
220 You can't set `Identity` or `Pinning`.
221 '';
222 default = { };
223 example = {
224 Datastore.StorageMax = "100GB";
225 Discovery.MDNS.Enabled = false;
226 Bootstrap = [
227 "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu"
228 "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm"
229 ];
230 Swarm.AddrFilters = null;
231 };
232
233 };
234
235 extraFlags = mkOption {
236 type = types.listOf types.str;
237 description = lib.mdDoc "Extra flags passed to the Kubo daemon";
238 default = [ ];
239 };
240
241 localDiscovery = mkOption {
242 type = types.bool;
243 description = lib.mdDoc ''Whether to enable local discovery for the Kubo daemon.
244 This will allow Kubo to scan ports on your local network. Some hosting services will ban you if you do this.
245 '';
246 default = false;
247 };
248
249 serviceFdlimit = mkOption {
250 type = types.nullOr types.int;
251 default = null;
252 description = lib.mdDoc "The fdlimit for the Kubo systemd unit or `null` to have the daemon attempt to manage it";
253 example = 64 * 1024;
254 };
255
256 startWhenNeeded = mkOption {
257 type = types.bool;
258 default = false;
259 description = lib.mdDoc "Whether to use socket activation to start Kubo when needed.";
260 };
261
262 };
263 };
264
265 ###### implementation
266
267 config = mkIf cfg.enable {
268 assertions = [
269 {
270 assertion = !builtins.hasAttr "Identity" cfg.settings;
271 message = ''
272 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.
273 '';
274 }
275 {
276 assertion = !((builtins.hasAttr "Pinning" cfg.settings) && (builtins.hasAttr "RemoteServices" cfg.settings.Pinning));
277 message = ''
278 You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
279 '';
280 }
281 ];
282
283 environment.systemPackages = [ cfg.package ];
284 environment.variables.IPFS_PATH = fakeKuboRepo;
285
286 # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
287 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
288
289 programs.fuse = mkIf cfg.autoMount {
290 userAllowOther = true;
291 };
292
293 users.users = mkIf (cfg.user == "ipfs") {
294 ipfs = {
295 group = cfg.group;
296 home = cfg.dataDir;
297 createHome = false;
298 uid = config.ids.uids.ipfs;
299 description = "IPFS daemon user";
300 packages = [
301 pkgs.kubo-migrator
302 ];
303 };
304 };
305
306 users.groups = mkIf (cfg.group == "ipfs") {
307 ipfs.gid = config.ids.gids.ipfs;
308 };
309
310 systemd.tmpfiles.rules = [
311 "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
312 ] ++ optionals cfg.autoMount [
313 "d '${cfg.ipfsMountDir}' - ${cfg.user} ${cfg.group} - -"
314 "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
315 ];
316
317 # The hardened systemd unit breaks the fuse-mount function according to documentation in the unit file itself
318 systemd.packages = if cfg.autoMount
319 then [ cfg.package.systemd_unit ]
320 else [ cfg.package.systemd_unit_hardened ];
321
322 services.kubo.settings = mkIf cfg.autoMount {
323 Mounts.FuseAllowOther = lib.mkDefault true;
324 Mounts.IPFS = lib.mkDefault cfg.ipfsMountDir;
325 Mounts.IPNS = lib.mkDefault cfg.ipnsMountDir;
326 };
327
328 systemd.services.ipfs = {
329 path = [ "/run/wrappers" cfg.package ];
330 environment.IPFS_PATH = cfg.dataDir;
331
332 preStart = ''
333 if [[ ! -f "$IPFS_PATH/config" ]]; then
334 ipfs init --empty-repo=${lib.boolToString cfg.emptyRepo}
335 else
336 # 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.
337 rm -vf "$IPFS_PATH/api"
338 '' + optionalString cfg.autoMigrate ''
339 ${pkgs.kubo-migrator}/bin/fs-repo-migrations -to '${cfg.package.repoVersion}' -y
340 '' + ''
341 fi
342 ipfs --offline config show |
343 ${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' |
344
345 # This command automatically injects the private key and other secrets from
346 # the old config file back into the new config file.
347 # Unfortunately, it doesn't keep the original `Identity.PeerID`,
348 # so we need `ipfs config show` and jq above.
349 # See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem.
350 # Kubo also wants a specific version of the original "Pinning.RemoteServices"
351 # section (redacted by `ipfs config show`), such that that section doesn't
352 # change when the changes are applied. Whyyyyyy.....
353 ipfs --offline config replace -
354 '';
355 postStop = mkIf cfg.autoMount ''
356 # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked
357 umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true
358 '';
359 serviceConfig = {
360 ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ];
361 User = cfg.user;
362 Group = cfg.group;
363 StateDirectory = "";
364 ReadWritePaths = optionals (!cfg.autoMount) [ "" cfg.dataDir ];
365 } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
366 } // optionalAttrs (!cfg.startWhenNeeded) {
367 wantedBy = [ "default.target" ];
368 };
369
370 systemd.sockets.ipfs-gateway = {
371 wantedBy = [ "sockets.target" ];
372 socketConfig = {
373 ListenStream =
374 [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
375 ListenDatagram =
376 [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
377 };
378 };
379
380 systemd.sockets.ipfs-api = {
381 wantedBy = [ "sockets.target" ];
382 socketConfig = {
383 # We also include "%t/ipfs.sock" because there is no way to put the "%t"
384 # in the multiaddr.
385 ListenStream =
386 [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
387 SocketMode = "0660";
388 SocketUser = cfg.user;
389 SocketGroup = cfg.group;
390 };
391 };
392 };
393
394 meta = {
395 maintainers = with lib.maintainers; [ Luflosi ];
396 };
397
398 imports = [
399 (mkRenamedOptionModule [ "services" "ipfs" "enable" ] [ "services" "kubo" "enable" ])
400 (mkRenamedOptionModule [ "services" "ipfs" "package" ] [ "services" "kubo" "package" ])
401 (mkRenamedOptionModule [ "services" "ipfs" "user" ] [ "services" "kubo" "user" ])
402 (mkRenamedOptionModule [ "services" "ipfs" "group" ] [ "services" "kubo" "group" ])
403 (mkRenamedOptionModule [ "services" "ipfs" "dataDir" ] [ "services" "kubo" "dataDir" ])
404 (mkRenamedOptionModule [ "services" "ipfs" "defaultMode" ] [ "services" "kubo" "defaultMode" ])
405 (mkRenamedOptionModule [ "services" "ipfs" "autoMount" ] [ "services" "kubo" "autoMount" ])
406 (mkRenamedOptionModule [ "services" "ipfs" "autoMigrate" ] [ "services" "kubo" "autoMigrate" ])
407 (mkRenamedOptionModule [ "services" "ipfs" "ipfsMountDir" ] [ "services" "kubo" "ipfsMountDir" ])
408 (mkRenamedOptionModule [ "services" "ipfs" "ipnsMountDir" ] [ "services" "kubo" "ipnsMountDir" ])
409 (mkRenamedOptionModule [ "services" "ipfs" "gatewayAddress" ] [ "services" "kubo" "settings" "Addresses" "Gateway" ])
410 (mkRenamedOptionModule [ "services" "ipfs" "apiAddress" ] [ "services" "kubo" "settings" "Addresses" "API" ])
411 (mkRenamedOptionModule [ "services" "ipfs" "swarmAddress" ] [ "services" "kubo" "settings" "Addresses" "Swarm" ])
412 (mkRenamedOptionModule [ "services" "ipfs" "enableGC" ] [ "services" "kubo" "enableGC" ])
413 (mkRenamedOptionModule [ "services" "ipfs" "emptyRepo" ] [ "services" "kubo" "emptyRepo" ])
414 (mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "settings" ])
415 (mkRenamedOptionModule [ "services" "ipfs" "extraFlags" ] [ "services" "kubo" "extraFlags" ])
416 (mkRenamedOptionModule [ "services" "ipfs" "localDiscovery" ] [ "services" "kubo" "localDiscovery" ])
417 (mkRenamedOptionModule [ "services" "ipfs" "serviceFdlimit" ] [ "services" "kubo" "serviceFdlimit" ])
418 (mkRenamedOptionModule [ "services" "ipfs" "startWhenNeeded" ] [ "services" "kubo" "startWhenNeeded" ])
419 (mkRenamedOptionModule [ "services" "kubo" "extraConfig" ] [ "services" "kubo" "settings" ])
420 (mkRenamedOptionModule [ "services" "kubo" "gatewayAddress" ] [ "services" "kubo" "settings" "Addresses" "Gateway" ])
421 (mkRenamedOptionModule [ "services" "kubo" "apiAddress" ] [ "services" "kubo" "settings" "Addresses" "API" ])
422 (mkRenamedOptionModule [ "services" "kubo" "swarmAddress" ] [ "services" "kubo" "settings" "Addresses" "Swarm" ])
423 ];
424}