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 and /ipns 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 }; 241 description = '' 242 Attrset of daemon configuration. 243 See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference. 244 You can't set `Identity` or `Pinning`. 245 ''; 246 default = { }; 247 example = { 248 Datastore.StorageMax = "100GB"; 249 Discovery.MDNS.Enabled = false; 250 Bootstrap = [ 251 "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu" 252 "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm" 253 ]; 254 Swarm.AddrFilters = null; 255 }; 256 257 }; 258 259 extraFlags = lib.mkOption { 260 type = lib.types.listOf lib.types.str; 261 description = "Extra flags passed to the Kubo daemon"; 262 default = [ ]; 263 }; 264 265 localDiscovery = lib.mkOption { 266 type = lib.types.bool; 267 description = '' 268 Whether to enable local discovery for the Kubo daemon. 269 This will allow Kubo to scan ports on your local network. Some hosting services will ban you if you do this. 270 ''; 271 default = false; 272 }; 273 274 serviceFdlimit = lib.mkOption { 275 type = lib.types.nullOr lib.types.int; 276 default = null; 277 description = "The fdlimit for the Kubo systemd unit or `null` to have the daemon attempt to manage it"; 278 example = 64 * 1024; 279 }; 280 281 startWhenNeeded = lib.mkOption { 282 type = lib.types.bool; 283 default = false; 284 description = "Whether to use socket activation to start Kubo when needed."; 285 }; 286 287 }; 288 }; 289 290 ###### implementation 291 292 config = lib.mkIf cfg.enable { 293 assertions = [ 294 { 295 assertion = !builtins.hasAttr "Identity" cfg.settings; 296 message = '' 297 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. 298 ''; 299 } 300 { 301 assertion = 302 !( 303 (builtins.hasAttr "Pinning" cfg.settings) 304 && (builtins.hasAttr "RemoteServices" cfg.settings.Pinning) 305 ); 306 message = '' 307 You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it. 308 ''; 309 } 310 { 311 assertion = 312 !( 313 (lib.versionAtLeast cfg.package.version "0.21") 314 && (builtins.hasAttr "Experimental" cfg.settings) 315 && (builtins.hasAttr "AcceleratedDHTClient" cfg.settings.Experimental) 316 ); 317 message = '' 318 The `services.kubo.settings.Experimental.AcceleratedDHTClient` option was renamed to `services.kubo.settings.Routing.AcceleratedDHTClient` in Kubo 0.21. 319 ''; 320 } 321 ]; 322 323 environment.systemPackages = [ cfg.package ]; 324 environment.variables.IPFS_PATH = fakeKuboRepo; 325 326 # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes 327 boot.kernel.sysctl."net.core.rmem_max" = lib.mkDefault 2500000; 328 boot.kernel.sysctl."net.core.wmem_max" = lib.mkDefault 2500000; 329 330 programs.fuse = lib.mkIf cfg.autoMount { 331 userAllowOther = true; 332 }; 333 334 users.users = lib.mkIf (cfg.user == "ipfs") { 335 ipfs = { 336 group = cfg.group; 337 home = cfg.dataDir; 338 createHome = false; 339 uid = config.ids.uids.ipfs; 340 description = "IPFS daemon user"; 341 packages = [ 342 pkgs.kubo-migrator 343 ]; 344 }; 345 }; 346 347 users.groups = lib.mkIf (cfg.group == "ipfs") { 348 ipfs.gid = config.ids.gids.ipfs; 349 }; 350 351 systemd.tmpfiles.settings."10-kubo" = 352 let 353 defaultConfig = { inherit (cfg) user group; }; 354 in 355 { 356 ${cfg.dataDir}.d = defaultConfig; 357 ${cfg.settings.Mounts.IPFS}.d = lib.mkIf (cfg.autoMount) defaultConfig; 358 ${cfg.settings.Mounts.IPNS}.d = lib.mkIf (cfg.autoMount) defaultConfig; 359 }; 360 361 # The hardened systemd unit breaks the fuse-mount function according to documentation in the unit file itself 362 systemd.packages = 363 if cfg.autoMount then [ cfg.package.systemd_unit ] else [ cfg.package.systemd_unit_hardened ]; 364 365 services.kubo.settings = lib.mkIf cfg.autoMount { 366 Mounts.FuseAllowOther = lib.mkDefault true; 367 }; 368 369 systemd.services.ipfs = 370 { 371 path = [ 372 "/run/wrappers" 373 cfg.package 374 ]; 375 environment.IPFS_PATH = cfg.dataDir; 376 377 preStart = 378 '' 379 if [[ ! -f "$IPFS_PATH/config" ]]; then 380 ipfs init --empty-repo=${lib.boolToString cfg.emptyRepo} 381 else 382 # 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. 383 rm -vf "$IPFS_PATH/api" 384 '' 385 + lib.optionalString cfg.autoMigrate '' 386 '${lib.getExe pkgs.kubo-migrator}' -to '${cfg.package.repoVersion}' -y 387 '' 388 + '' 389 fi 390 ipfs --offline config show | 391 ${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' | 392 393 # This command automatically injects the private key and other secrets from 394 # the old config file back into the new config file. 395 # Unfortunately, it doesn't keep the original `Identity.PeerID`, 396 # so we need `ipfs config show` and jq above. 397 # See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem. 398 # Kubo also wants a specific version of the original "Pinning.RemoteServices" 399 # section (redacted by `ipfs config show`), such that that section doesn't 400 # change when the changes are applied. Whyyyyyy..... 401 ipfs --offline config replace - 402 ''; 403 postStop = lib.mkIf cfg.autoMount '' 404 # After an unclean shutdown the fuse mounts at cfg.settings.Mounts.IPFS and cfg.settings.Mounts.IPNS are locked 405 umount --quiet '${cfg.settings.Mounts.IPFS}' '${cfg.settings.Mounts.IPNS}' || true 406 ''; 407 serviceConfig = { 408 ExecStart = [ 409 "" 410 "${cfg.package}/bin/ipfs daemon ${kuboFlags}" 411 ]; 412 User = cfg.user; 413 Group = cfg.group; 414 StateDirectory = ""; 415 ReadWritePaths = lib.optionals (!cfg.autoMount) [ 416 "" 417 cfg.dataDir 418 ]; 419 # Make sure the socket units are started before ipfs.service 420 Sockets = [ 421 "ipfs-gateway.socket" 422 "ipfs-api.socket" 423 ]; 424 } // lib.optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; }; 425 } 426 // lib.optionalAttrs (!cfg.startWhenNeeded) { 427 wantedBy = [ "default.target" ]; 428 }; 429 430 systemd.sockets.ipfs-gateway = { 431 wantedBy = [ "sockets.target" ]; 432 socketConfig = { 433 ListenStream = [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway); 434 ListenDatagram = [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway); 435 }; 436 }; 437 438 systemd.sockets.ipfs-api = { 439 wantedBy = [ "sockets.target" ]; 440 socketConfig = { 441 # We also include "%t/ipfs.sock" because there is no way to put the "%t" 442 # in the multiaddr. 443 ListenStream = [ 444 "" 445 "%t/ipfs.sock" 446 ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API); 447 SocketMode = "0660"; 448 SocketUser = cfg.user; 449 SocketGroup = cfg.group; 450 }; 451 }; 452 }; 453 454 meta = { 455 maintainers = with lib.maintainers; [ Luflosi ]; 456 }; 457 458 imports = [ 459 (lib.mkRenamedOptionModule [ "services" "ipfs" "enable" ] [ "services" "kubo" "enable" ]) 460 (lib.mkRenamedOptionModule [ "services" "ipfs" "package" ] [ "services" "kubo" "package" ]) 461 (lib.mkRenamedOptionModule [ "services" "ipfs" "user" ] [ "services" "kubo" "user" ]) 462 (lib.mkRenamedOptionModule [ "services" "ipfs" "group" ] [ "services" "kubo" "group" ]) 463 (lib.mkRenamedOptionModule [ "services" "ipfs" "dataDir" ] [ "services" "kubo" "dataDir" ]) 464 (lib.mkRenamedOptionModule [ "services" "ipfs" "defaultMode" ] [ "services" "kubo" "defaultMode" ]) 465 (lib.mkRenamedOptionModule [ "services" "ipfs" "autoMount" ] [ "services" "kubo" "autoMount" ]) 466 (lib.mkRenamedOptionModule [ "services" "ipfs" "autoMigrate" ] [ "services" "kubo" "autoMigrate" ]) 467 (lib.mkRenamedOptionModule 468 [ "services" "ipfs" "ipfsMountDir" ] 469 [ "services" "kubo" "settings" "Mounts" "IPFS" ] 470 ) 471 (lib.mkRenamedOptionModule 472 [ "services" "ipfs" "ipnsMountDir" ] 473 [ "services" "kubo" "settings" "Mounts" "IPNS" ] 474 ) 475 (lib.mkRenamedOptionModule 476 [ "services" "ipfs" "gatewayAddress" ] 477 [ "services" "kubo" "settings" "Addresses" "Gateway" ] 478 ) 479 (lib.mkRenamedOptionModule 480 [ "services" "ipfs" "apiAddress" ] 481 [ "services" "kubo" "settings" "Addresses" "API" ] 482 ) 483 (lib.mkRenamedOptionModule 484 [ "services" "ipfs" "swarmAddress" ] 485 [ "services" "kubo" "settings" "Addresses" "Swarm" ] 486 ) 487 (lib.mkRenamedOptionModule [ "services" "ipfs" "enableGC" ] [ "services" "kubo" "enableGC" ]) 488 (lib.mkRenamedOptionModule [ "services" "ipfs" "emptyRepo" ] [ "services" "kubo" "emptyRepo" ]) 489 (lib.mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "settings" ]) 490 (lib.mkRenamedOptionModule [ "services" "ipfs" "extraFlags" ] [ "services" "kubo" "extraFlags" ]) 491 (lib.mkRenamedOptionModule 492 [ "services" "ipfs" "localDiscovery" ] 493 [ "services" "kubo" "localDiscovery" ] 494 ) 495 (lib.mkRenamedOptionModule 496 [ "services" "ipfs" "serviceFdlimit" ] 497 [ "services" "kubo" "serviceFdlimit" ] 498 ) 499 (lib.mkRenamedOptionModule 500 [ "services" "ipfs" "startWhenNeeded" ] 501 [ "services" "kubo" "startWhenNeeded" ] 502 ) 503 (lib.mkRenamedOptionModule [ "services" "kubo" "extraConfig" ] [ "services" "kubo" "settings" ]) 504 (lib.mkRenamedOptionModule 505 [ "services" "kubo" "gatewayAddress" ] 506 [ "services" "kubo" "settings" "Addresses" "Gateway" ] 507 ) 508 (lib.mkRenamedOptionModule 509 [ "services" "kubo" "apiAddress" ] 510 [ "services" "kubo" "settings" "Addresses" "API" ] 511 ) 512 (lib.mkRenamedOptionModule 513 [ "services" "kubo" "swarmAddress" ] 514 [ "services" "kubo" "settings" "Addresses" "Swarm" ] 515 ) 516 (lib.mkRenamedOptionModule 517 [ "services" "kubo" "ipfsMountDir" ] 518 [ "services" "kubo" "settings" "Mounts" "IPFS" ] 519 ) 520 (lib.mkRenamedOptionModule 521 [ "services" "kubo" "ipnsMountDir" ] 522 [ "services" "kubo" "settings" "Mounts" "IPNS" ] 523 ) 524 ]; 525}