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}