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}