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}