1{ config, lib, pkgs, options, ... }: 2with lib; 3let 4 cfg = config.services.ipfs; 5 opt = options.services.ipfs; 6 7 ipfsFlags = toString ([ 8 (optionalString cfg.autoMount "--mount") 9 (optionalString cfg.enableGC "--enable-gc") 10 (optionalString (cfg.serviceFdlimit != null) "--manage-fdlimit=false") 11 (optionalString (cfg.defaultMode == "offline") "--offline") 12 (optionalString (cfg.defaultMode == "norouting") "--routing=none") 13 ] ++ cfg.extraFlags); 14 15 splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw); 16 17 multiaddrToListenStream = addrRaw: let 18 addr = splitMulitaddr addrRaw; 19 s = builtins.elemAt addr; 20 in if s 0 == "ip4" && s 2 == "tcp" 21 then "${s 1}:${s 3}" 22 else if s 0 == "ip6" && s 2 == "tcp" 23 then "[${s 1}]:${s 3}" 24 else if s 0 == "unix" 25 then "/${lib.concatStringsSep "/" (lib.tail addr)}" 26 else null; # not valid for listen stream, skip 27 28 multiaddrToListenDatagram = addrRaw: let 29 addr = splitMulitaddr addrRaw; 30 s = builtins.elemAt addr; 31 in if s 0 == "ip4" && s 2 == "udp" 32 then "${s 1}:${s 3}" 33 else if s 0 == "ip6" && s 2 == "udp" 34 then "[${s 1}]:${s 3}" 35 else null; # not valid for listen datagram, skip 36 37in { 38 39 ###### interface 40 41 options = { 42 43 services.ipfs = { 44 45 enable = mkEnableOption "Interplanetary File System (WARNING: may cause severe network degredation)"; 46 47 package = mkOption { 48 type = types.package; 49 default = pkgs.ipfs; 50 defaultText = "pkgs.ipfs"; 51 description = "Which IPFS package to use."; 52 }; 53 54 user = mkOption { 55 type = types.str; 56 default = "ipfs"; 57 description = "User under which the IPFS daemon runs"; 58 }; 59 60 group = mkOption { 61 type = types.str; 62 default = "ipfs"; 63 description = "Group under which the IPFS daemon runs"; 64 }; 65 66 dataDir = mkOption { 67 type = types.str; 68 default = if versionAtLeast config.system.stateVersion "17.09" 69 then "/var/lib/ipfs" 70 else "/var/lib/ipfs/.ipfs"; 71 description = "The data dir for IPFS"; 72 }; 73 74 defaultMode = mkOption { 75 type = types.enum [ "online" "offline" "norouting" ]; 76 default = "online"; 77 description = "systemd service that is enabled by default"; 78 }; 79 80 autoMount = mkOption { 81 type = types.bool; 82 default = false; 83 description = "Whether IPFS should try to mount /ipfs and /ipns at startup."; 84 }; 85 86 ipfsMountDir = mkOption { 87 type = types.str; 88 default = "/ipfs"; 89 description = "Where to mount the IPFS namespace to"; 90 }; 91 92 ipnsMountDir = mkOption { 93 type = types.str; 94 default = "/ipns"; 95 description = "Where to mount the IPNS namespace to"; 96 }; 97 98 gatewayAddress = mkOption { 99 type = types.str; 100 default = "/ip4/127.0.0.1/tcp/8080"; 101 description = "Where the IPFS Gateway can be reached"; 102 }; 103 104 apiAddress = mkOption { 105 type = types.str; 106 default = "/ip4/127.0.0.1/tcp/5001"; 107 description = "Where IPFS exposes its API to"; 108 }; 109 110 swarmAddress = mkOption { 111 type = types.listOf types.str; 112 default = [ 113 "/ip4/0.0.0.0/tcp/4001" 114 "/ip6/::/tcp/4001" 115 "/ip4/0.0.0.0/udp/4001/quic" 116 "/ip6/::/udp/4001/quic" 117 ]; 118 description = "Where IPFS listens for incoming p2p connections"; 119 }; 120 121 enableGC = mkOption { 122 type = types.bool; 123 default = false; 124 description = "Whether to enable automatic garbage collection"; 125 }; 126 127 emptyRepo = mkOption { 128 type = types.bool; 129 default = false; 130 description = "If set to true, the repo won't be initialized with help files"; 131 }; 132 133 extraConfig = mkOption { 134 type = types.attrs; 135 description = '' 136 Attrset of daemon configuration to set using <command>ipfs config</command>, every time the daemon starts. 137 These are applied last, so may override configuration set by other options in this module. 138 Keep in mind that this configuration is stateful; i.e., unsetting anything in here does not reset the value to the default! 139 ''; 140 default = {}; 141 example = { 142 Datastore.StorageMax = "100GB"; 143 Discovery.MDNS.Enabled = false; 144 Bootstrap = [ 145 "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu" 146 "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm" 147 ]; 148 Swarm.AddrFilters = null; 149 }; 150 151 }; 152 153 extraFlags = mkOption { 154 type = types.listOf types.str; 155 description = "Extra flags passed to the IPFS daemon"; 156 default = []; 157 }; 158 159 localDiscovery = mkOption { 160 type = types.bool; 161 description = ''Whether to enable local discovery for the ipfs daemon. 162 This will allow ipfs to scan ports on your local network. Some hosting services will ban you if you do this. 163 ''; 164 default = true; 165 }; 166 167 serviceFdlimit = mkOption { 168 type = types.nullOr types.int; 169 default = null; 170 description = "The fdlimit for the IPFS systemd unit or <literal>null</literal> to have the daemon attempt to manage it"; 171 example = 64*1024; 172 }; 173 174 startWhenNeeded = mkOption { 175 type = types.bool; 176 default = false; 177 description = "Whether to use socket activation to start IPFS when needed."; 178 }; 179 180 }; 181 }; 182 183 ###### implementation 184 185 config = mkIf cfg.enable { 186 environment.systemPackages = [ cfg.package ]; 187 environment.variables.IPFS_PATH = cfg.dataDir; 188 189 programs.fuse = mkIf cfg.autoMount { 190 userAllowOther = true; 191 }; 192 193 users.users = mkIf (cfg.user == "ipfs") { 194 ipfs = { 195 group = cfg.group; 196 home = cfg.dataDir; 197 createHome = false; 198 uid = config.ids.uids.ipfs; 199 description = "IPFS daemon user"; 200 packages = [ 201 pkgs.ipfs-migrator 202 ]; 203 }; 204 }; 205 206 users.groups = mkIf (cfg.group == "ipfs") { 207 ipfs.gid = config.ids.gids.ipfs; 208 }; 209 210 systemd.tmpfiles.rules = [ 211 "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -" 212 ] ++ optionals cfg.autoMount [ 213 "d '${cfg.ipfsMountDir}' - ${cfg.user} ${cfg.group} - -" 214 "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -" 215 ]; 216 217 systemd.packages = [ cfg.package ]; 218 219 systemd.services.ipfs = { 220 path = [ "/run/wrappers" cfg.package ]; 221 environment.IPFS_PATH = cfg.dataDir; 222 223 preStart = '' 224 if [[ ! -f ${cfg.dataDir}/config ]]; then 225 ipfs init ${optionalString cfg.emptyRepo "-e"} \ 226 ${optionalString (! cfg.localDiscovery) "--profile=server"} 227 else 228 ${if cfg.localDiscovery 229 then "ipfs config profile apply local-discovery" 230 else "ipfs config profile apply server" 231 } 232 fi 233 '' + optionalString cfg.autoMount '' 234 ipfs --local config Mounts.FuseAllowOther --json true 235 ipfs --local config Mounts.IPFS ${cfg.ipfsMountDir} 236 ipfs --local config Mounts.IPNS ${cfg.ipnsMountDir} 237 '' + concatStringsSep "\n" (collect 238 isString 239 (mapAttrsRecursive 240 (path: value: 241 # Using heredoc below so that the value is never improperly quoted 242 '' 243 read value <<EOF 244 ${builtins.toJSON value} 245 EOF 246 ipfs --local config --json "${concatStringsSep "." path}" "$value" 247 '') 248 ({ Addresses.API = cfg.apiAddress; 249 Addresses.Gateway = cfg.gatewayAddress; 250 Addresses.Swarm = cfg.swarmAddress; 251 } // 252 cfg.extraConfig)) 253 ); 254 serviceConfig = { 255 ExecStart = ["" "${cfg.package}/bin/ipfs daemon ${ipfsFlags}"]; 256 User = cfg.user; 257 Group = cfg.group; 258 } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; }; 259 } // optionalAttrs (!cfg.startWhenNeeded) { 260 wantedBy = [ "default.target" ]; 261 }; 262 263 systemd.sockets.ipfs-gateway = { 264 wantedBy = [ "sockets.target" ]; 265 socketConfig = { 266 ListenStream = let 267 fromCfg = multiaddrToListenStream cfg.gatewayAddress; 268 in [ "" ] ++ lib.optional (fromCfg != null) fromCfg; 269 ListenDatagram = let 270 fromCfg = multiaddrToListenDatagram cfg.gatewayAddress; 271 in [ "" ] ++ lib.optional (fromCfg != null) fromCfg; 272 }; 273 }; 274 275 systemd.sockets.ipfs-api = { 276 wantedBy = [ "sockets.target" ]; 277 # We also include "%t/ipfs.sock" because there is no way to put the "%t" 278 # in the multiaddr. 279 socketConfig.ListenStream = let 280 fromCfg = multiaddrToListenStream cfg.apiAddress; 281 in [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg; 282 }; 283 284 }; 285}