vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / syncthing.nix
blob2d32cf4517062e744638ec2aeb4717c70080cdc7
1 { config, lib, options, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.syncthing;
7   opt = options.services.syncthing;
8   defaultUser = "syncthing";
9   defaultGroup = defaultUser;
10   settingsFormat = pkgs.formats.json { };
11   cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != {})) cfg.settings;
13   isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/";
15   # Syncthing supports serving the GUI over Unix sockets. If that happens, the
16   # API is served over the Unix socket as well.  This function returns the correct
17   # curl arguments for the address portion of the curl command for both network
18   # and Unix socket addresses.
19   curlAddressArgs = path: if isUnixGui
20     # if cfg.guiAddress is a unix socket, tell curl explicitly about it
21     # note that the dot in front of `${path}` is the hostname, which is
22     # required.
23     then "--unix-socket ${cfg.guiAddress} http://.${path}"
24     # no adjustements are needed if cfg.guiAddress is a network address
25     else "${cfg.guiAddress}${path}"
26     ;
28   devices = mapAttrsToList (_: device: device // {
29     deviceID = device.id;
30   }) cfg.settings.devices;
32   folders = mapAttrsToList (_: folder: folder //
33     throwIf (folder?rescanInterval || folder?watch || folder?watchDelay) ''
34       The options services.syncthing.settings.folders.<name>.{rescanInterval,watch,watchDelay}
35       were removed. Please use, respectively, {rescanIntervalS,fsWatcherEnabled,fsWatcherDelayS} instead.
36     '' {
37     devices = map (device:
38       if builtins.isString device then
39         { deviceId = cfg.settings.devices.${device}.id; }
40       else
41         device
42     ) folder.devices;
43   }) (filterAttrs (_: folder:
44     folder.enable
45   ) cfg.settings.folders);
47   jq = "${pkgs.jq}/bin/jq";
48   updateConfig = pkgs.writers.writeBash "merge-syncthing-config" (''
49     set -efu
51     # be careful not to leak secrets in the filesystem or in process listings
52     umask 0077
54     curl() {
55         # get the api key by parsing the config.xml
56         while
57             ! ${pkgs.libxml2}/bin/xmllint \
58                 --xpath 'string(configuration/gui/apikey)' \
59                 ${cfg.configDir}/config.xml \
60                 >"$RUNTIME_DIRECTORY/api_key"
61         do sleep 1; done
62         (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
63         ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
64             --retry 1000 --retry-delay 1 --retry-all-errors \
65             "$@"
66     }
67   '' +
69   /* Syncthing's rest API for the folders and devices is almost identical.
70   Hence we iterate them using lib.pipe and generate shell commands for both at
71   the sime time. */
72   (lib.pipe {
73     # The attributes below are the only ones that are different for devices /
74     # folders.
75     devs = {
76       new_conf_IDs = map (v: v.id) devices;
77       GET_IdAttrName = "deviceID";
78       override = cfg.overrideDevices;
79       conf = devices;
80       baseAddress = curlAddressArgs "/rest/config/devices";
81     };
82     dirs = {
83       new_conf_IDs = map (v: v.id) folders;
84       GET_IdAttrName = "id";
85       override = cfg.overrideFolders;
86       conf = folders;
87       baseAddress = curlAddressArgs "/rest/config/folders";
88     };
89   } [
90     # Now for each of these attributes, write the curl commands that are
91     # identical to both folders and devices.
92     (mapAttrs (conf_type: s:
93       # We iterate the `conf` list now, and run a curl -X POST command for each, that
94       # should update that device/folder only.
95       lib.pipe s.conf [
96         # Quoting https://docs.syncthing.net/rest/config.html:
97         #
98         # > PUT takes an array and POST a single object. In both cases if a
99         # given folder/device already exists, it’s replaced, otherwise a new
100         # one is added.
101         #
102         # What's not documented, is that using PUT will remove objects that
103         # don't exist in the array given. That's why we use here `POST`, and
104         # only if s.override == true then we DELETE the relevant folders
105         # afterwards.
106         (map (new_cfg: ''
107           curl -d ${lib.escapeShellArg (builtins.toJSON new_cfg)} -X POST ${s.baseAddress}
108         ''))
109         (lib.concatStringsSep "\n")
110       ]
111       /* If we need to override devices/folders, we iterate all currently configured
112       IDs, via another `curl -X GET`, and we delete all IDs that are not part of
113       the Nix configured list of IDs
114       */
115       + lib.optionalString s.override ''
116         stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \
117           --argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \
118           --raw-output \
119           '[.[].${s.GET_IdAttrName}] - $new_ids | .[]'
120         )"
121         for id in ''${stale_${conf_type}_ids}; do
122           curl -X DELETE ${s.baseAddress}/$id
123         done
124       ''
125     ))
126     builtins.attrValues
127     (lib.concatStringsSep "\n")
128   ]) +
129   /* Now we update the other settings defined in cleanedConfig which are not
130   "folders" or "devices". */
131   (lib.pipe cleanedConfig [
132     builtins.attrNames
133     (lib.subtractLists ["folders" "devices"])
134     (map (subOption: ''
135       curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"}
136     ''))
137     (lib.concatStringsSep "\n")
138   ]) + ''
139     # restart Syncthing if required
140     if curl ${curlAddressArgs "/rest/config/restart-required"} |
141        ${jq} -e .requiresRestart > /dev/null; then
142         curl -X POST ${curlAddressArgs "/rest/system/restart"}
143     fi
144   '');
145 in {
146   ###### interface
147   options = {
148     services.syncthing = {
150       enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
152       cert = mkOption {
153         type = types.nullOr types.str;
154         default = null;
155         description = ''
156           Path to the `cert.pem` file, which will be copied into Syncthing's
157           [configDir](#opt-services.syncthing.configDir).
158         '';
159       };
161       key = mkOption {
162         type = types.nullOr types.str;
163         default = null;
164         description = ''
165           Path to the `key.pem` file, which will be copied into Syncthing's
166           [configDir](#opt-services.syncthing.configDir).
167         '';
168       };
170       overrideDevices = mkOption {
171         type = types.bool;
172         default = true;
173         description = ''
174           Whether to delete the devices which are not configured via the
175           [devices](#opt-services.syncthing.settings.devices) option.
176           If set to `false`, devices added via the web
177           interface will persist and will have to be deleted manually.
178         '';
179       };
181       overrideFolders = mkOption {
182         type = types.bool;
183         default = true;
184         description = ''
185           Whether to delete the folders which are not configured via the
186           [folders](#opt-services.syncthing.settings.folders) option.
187           If set to `false`, folders added via the web
188           interface will persist and will have to be deleted manually.
189         '';
190       };
192       settings = mkOption {
193         type = types.submodule {
194           freeformType = settingsFormat.type;
195           options = {
196             # global options
197             options = mkOption {
198               default = {};
199               description = ''
200                 The options element contains all other global configuration options
201               '';
202               type = types.submodule ({ name, ... }: {
203                 freeformType = settingsFormat.type;
204                 options = {
205                   localAnnounceEnabled = mkOption {
206                     type = types.nullOr types.bool;
207                     default = null;
208                     description = ''
209                       Whether to send announcements to the local LAN, also use such announcements to find other devices.
210                     '';
211                   };
213                   localAnnouncePort = mkOption {
214                     type = types.nullOr types.int;
215                     default = null;
216                     description = ''
217                       The port on which to listen and send IPv4 broadcast announcements to.
218                     '';
219                   };
221                   relaysEnabled = mkOption {
222                     type = types.nullOr types.bool;
223                     default = null;
224                     description = ''
225                       When true, relays will be connected to and potentially used for device to device connections.
226                     '';
227                   };
229                   urAccepted = mkOption {
230                     type = types.nullOr types.int;
231                     default = null;
232                     description = ''
233                       Whether the user has accepted to submit anonymous usage data.
234                       The default, 0, mean the user has not made a choice, and Syncthing will ask at some point in the future.
235                       "-1" means no, a number above zero means that that version of usage reporting has been accepted.
236                     '';
237                   };
239                   limitBandwidthInLan = mkOption {
240                     type = types.nullOr types.bool;
241                     default = null;
242                     description = ''
243                       Whether to apply bandwidth limits to devices in the same broadcast domain as the local device.
244                     '';
245                   };
247                   maxFolderConcurrency = mkOption {
248                     type = types.nullOr types.int;
249                     default = null;
250                     description = ''
251                       This option controls how many folders may concurrently be in I/O-intensive operations such as syncing or scanning.
252                       The mechanism is described in detail in a [separate chapter](https://docs.syncthing.net/advanced/option-max-concurrency.html).
253                     '';
254                   };
255                 };
256               });
257             };
259             # device settings
260             devices = mkOption {
261               default = {};
262               description = ''
263                 Peers/devices which Syncthing should communicate with.
265                 Note that you can still add devices manually, but those changes
266                 will be reverted on restart if [overrideDevices](#opt-services.syncthing.overrideDevices)
267                 is enabled.
268               '';
269               example = {
270                 bigbox = {
271                   id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
272                   addresses = [ "tcp://192.168.0.10:51820" ];
273                 };
274               };
275               type = types.attrsOf (types.submodule ({ name, ... }: {
276                 freeformType = settingsFormat.type;
277                 options = {
279                   name = mkOption {
280                     type = types.str;
281                     default = name;
282                     description = ''
283                       The name of the device.
284                     '';
285                   };
287                   id = mkOption {
288                     type = types.str;
289                     description = ''
290                       The device ID. See <https://docs.syncthing.net/dev/device-ids.html>.
291                     '';
292                   };
294                   autoAcceptFolders = mkOption {
295                     type = types.bool;
296                     default = false;
297                     description = ''
298                       Automatically create or share folders that this device advertises at the default path.
299                       See <https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format>.
300                     '';
301                   };
303                 };
304               }));
305             };
307             # folder settings
308             folders = mkOption {
309               default = {};
310               description = ''
311                 Folders which should be shared by Syncthing.
313                 Note that you can still add folders manually, but those changes
314                 will be reverted on restart if [overrideFolders](#opt-services.syncthing.overrideFolders)
315                 is enabled.
316               '';
317               example = literalExpression ''
318                 {
319                   "/home/user/sync" = {
320                     id = "syncme";
321                     devices = [ "bigbox" ];
322                   };
323                 }
324               '';
325               type = types.attrsOf (types.submodule ({ name, ... }: {
326                 freeformType = settingsFormat.type;
327                 options = {
329                   enable = mkOption {
330                     type = types.bool;
331                     default = true;
332                     description = ''
333                       Whether to share this folder.
334                       This option is useful when you want to define all folders
335                       in one place, but not every machine should share all folders.
336                     '';
337                   };
339                   path = mkOption {
340                     # TODO for release 23.05: allow relative paths again and set
341                     # working directory to cfg.dataDir
342                     type = types.str // {
343                       check = x: types.str.check x && (substring 0 1 x == "/" || substring 0 2 x == "~/");
344                       description = types.str.description + " starting with / or ~/";
345                     };
346                     default = name;
347                     description = ''
348                       The path to the folder which should be shared.
349                       Only absolute paths (starting with `/`) and paths relative to
350                       the [user](#opt-services.syncthing.user)'s home directory
351                       (starting with `~/`) are allowed.
352                     '';
353                   };
355                   id = mkOption {
356                     type = types.str;
357                     default = name;
358                     description = ''
359                       The ID of the folder. Must be the same on all devices.
360                     '';
361                   };
363                   label = mkOption {
364                     type = types.str;
365                     default = name;
366                     description = ''
367                       The label of the folder.
368                     '';
369                   };
371                   type = mkOption {
372                     type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ];
373                     default = "sendreceive";
374                     description = ''
375                       Controls how the folder is handled by Syncthing.
376                       See <https://docs.syncthing.net/users/config.html#config-option-folder.type>.
377                     '';
378                   };
380                   devices = mkOption {
381                     type = types.listOf types.str;
382                     default = [];
383                     description = ''
384                       The devices this folder should be shared with. Each device must
385                       be defined in the [devices](#opt-services.syncthing.settings.devices) option.
386                     '';
387                   };
389                   versioning = mkOption {
390                     default = null;
391                     description = ''
392                       How to keep changed/deleted files with Syncthing.
393                       There are 4 different types of versioning with different parameters.
394                       See <https://docs.syncthing.net/users/versioning.html>.
395                     '';
396                     example = literalExpression ''
397                       [
398                         {
399                           versioning = {
400                             type = "simple";
401                             params.keep = "10";
402                           };
403                         }
404                         {
405                           versioning = {
406                             type = "trashcan";
407                             params.cleanoutDays = "1000";
408                           };
409                         }
410                         {
411                           versioning = {
412                             type = "staggered";
413                             fsPath = "/syncthing/backup";
414                             params = {
415                               cleanInterval = "3600";
416                               maxAge = "31536000";
417                             };
418                           };
419                         }
420                         {
421                           versioning = {
422                             type = "external";
423                             params.versionsPath = pkgs.writers.writeBash "backup" '''
424                               folderpath="$1"
425                               filepath="$2"
426                               rm -rf "$folderpath/$filepath"
427                             ''';
428                           };
429                         }
430                       ]
431                     '';
432                     type = with types; nullOr (submodule {
433                       freeformType = settingsFormat.type;
434                       options = {
435                         type = mkOption {
436                           type = enum [ "external" "simple" "staggered" "trashcan" ];
437                           description = ''
438                             The type of versioning.
439                             See <https://docs.syncthing.net/users/versioning.html>.
440                           '';
441                         };
442                       };
443                     });
444                   };
446                   copyOwnershipFromParent = mkOption {
447                     type = types.bool;
448                     default = false;
449                     description = ''
450                       On Unix systems, tries to copy file/folder ownership from the parent directory (the directory it’s located in).
451                       Requires running Syncthing as a privileged user, or granting it additional capabilities (e.g. CAP_CHOWN on Linux).
452                     '';
453                   };
454                 };
455               }));
456             };
458           };
459         };
460         default = {};
461         description = ''
462           Extra configuration options for Syncthing.
463           See <https://docs.syncthing.net/users/config.html>.
464           Note that this attribute set does not exactly match the documented
465           xml format. Instead, this is the format of the json rest api. There
466           are slight differences. For example, this xml:
467           ```xml
468           <options>
469             <listenAddress>default</listenAddress>
470             <minHomeDiskFree unit="%">1</minHomeDiskFree>
471           </options>
472           ```
473           corresponds to the json:
474           ```json
475           {
476             options: {
477               listenAddresses = [
478                 "default"
479               ];
480               minHomeDiskFree = {
481                 unit = "%";
482                 value = 1;
483               };
484             };
485           }
486           ```
487         '';
488         example = {
489           options.localAnnounceEnabled = false;
490           gui.theme = "black";
491         };
492       };
494       guiAddress = mkOption {
495         type = types.str;
496         default = "127.0.0.1:8384";
497         description = ''
498           The address to serve the web interface at.
499         '';
500       };
502       systemService = mkOption {
503         type = types.bool;
504         default = true;
505         description = ''
506           Whether to auto-launch Syncthing as a system service.
507         '';
508       };
510       user = mkOption {
511         type = types.str;
512         default = defaultUser;
513         example = "yourUser";
514         description = ''
515           The user to run Syncthing as.
516           By default, a user named `${defaultUser}` will be created whose home
517           directory is [dataDir](#opt-services.syncthing.dataDir).
518         '';
519       };
521       group = mkOption {
522         type = types.str;
523         default = defaultGroup;
524         example = "yourGroup";
525         description = ''
526           The group to run Syncthing under.
527           By default, a group named `${defaultGroup}` will be created.
528         '';
529       };
531       all_proxy = mkOption {
532         type = with types; nullOr str;
533         default = null;
534         example = "socks5://address.com:1234";
535         description = ''
536           Overwrites the all_proxy environment variable for the Syncthing process to
537           the given value. This is normally used to let Syncthing connect
538           through a SOCKS5 proxy server.
539           See <https://docs.syncthing.net/users/proxying.html>.
540         '';
541       };
543       dataDir = mkOption {
544         type = types.path;
545         default = "/var/lib/syncthing";
546         example = "/home/yourUser";
547         description = ''
548           The path where synchronised directories will exist.
549         '';
550       };
552       configDir = let
553         cond = versionAtLeast config.system.stateVersion "19.03";
554       in mkOption {
555         type = types.path;
556         description = ''
557           The path where the settings and keys will exist.
558         '';
559         default = cfg.dataDir + optionalString cond "/.config/syncthing";
560         defaultText = literalMD ''
561           * if `stateVersion >= 19.03`:
563                 config.${opt.dataDir} + "/.config/syncthing"
564           * otherwise:
566                 config.${opt.dataDir}
567         '';
568       };
570       databaseDir = mkOption {
571         type = types.path;
572         description = ''
573           The directory containing the database and logs.
574         '';
575         default = cfg.configDir;
576         defaultText = literalExpression "config.${opt.configDir}";
577       };
579       extraFlags = mkOption {
580         type = types.listOf types.str;
581         default = [];
582         example = [ "--reset-deltas" ];
583         description = ''
584           Extra flags passed to the syncthing command in the service definition.
585         '';
586       };
588       openDefaultPorts = mkOption {
589         type = types.bool;
590         default = false;
591         example = true;
592         description = ''
593           Whether to open the default ports in the firewall: TCP/UDP 22000 for transfers
594           and UDP 21027 for discovery.
596           If multiple users are running Syncthing on this machine, you will need
597           to manually open a set of ports for each instance and leave this disabled.
598           Alternatively, if you are running only a single instance on this machine
599           using the default ports, enable this.
600         '';
601       };
603       package = mkPackageOption pkgs "syncthing" { };
604     };
605   };
607   imports = [
608     (mkRemovedOptionModule [ "services" "syncthing" "useInotify" ] ''
609       This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher".
610       It can be enabled on a per-folder basis through the web interface.
611     '')
612     (mkRenamedOptionModule [ "services" "syncthing" "extraOptions" ] [ "services" "syncthing" "settings" ])
613     (mkRenamedOptionModule [ "services" "syncthing" "folders" ] [ "services" "syncthing" "settings" "folders" ])
614     (mkRenamedOptionModule [ "services" "syncthing" "devices" ] [ "services" "syncthing" "settings" "devices" ])
615     (mkRenamedOptionModule [ "services" "syncthing" "options" ] [ "services" "syncthing" "settings" "options" ])
616   ] ++ map (o:
617     mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ]
618   ) [ "cert" "key" "devices" "folders" "overrideDevices" "overrideFolders" "extraOptions"];
620   ###### implementation
622   config = mkIf cfg.enable {
624     networking.firewall = mkIf cfg.openDefaultPorts {
625       allowedTCPPorts = [ 22000 ];
626       allowedUDPPorts = [ 21027 22000 ];
627     };
629     systemd.packages = [ pkgs.syncthing ];
631     users.users = mkIf (cfg.systemService && cfg.user == defaultUser) {
632       ${defaultUser} =
633         { group = cfg.group;
634           home  = cfg.dataDir;
635           createHome = true;
636           uid = config.ids.uids.syncthing;
637           description = "Syncthing daemon user";
638         };
639     };
641     users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) {
642       ${defaultGroup}.gid =
643         config.ids.gids.syncthing;
644     };
646     systemd.services = {
647       # upstream reference:
648       # https://github.com/syncthing/syncthing/blob/main/etc/linux-systemd/system/syncthing%40.service
649       syncthing = mkIf cfg.systemService {
650         description = "Syncthing service";
651         after = [ "network.target" ];
652         environment = {
653           STNORESTART = "yes";
654           STNOUPGRADE = "yes";
655           inherit (cfg) all_proxy;
656         } // config.networking.proxy.envVars;
657         wantedBy = [ "multi-user.target" ];
658         serviceConfig = {
659           Restart = "on-failure";
660           SuccessExitStatus = "3 4";
661           RestartForceExitStatus="3 4";
662           User = cfg.user;
663           Group = cfg.group;
664           ExecStartPre = mkIf (cfg.cert != null || cfg.key != null)
665             "+${pkgs.writers.writeBash "syncthing-copy-keys" ''
666               install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir}
667               ${optionalString (cfg.cert != null) ''
668                 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.cert} ${cfg.configDir}/cert.pem
669               ''}
670               ${optionalString (cfg.key != null) ''
671                 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem
672               ''}
673             ''}"
674           ;
675           ExecStart = ''
676             ${cfg.package}/bin/syncthing \
677               -no-browser \
678               -gui-address=${if isUnixGui then "unix://" else ""}${cfg.guiAddress} \
679               -config=${cfg.configDir} \
680               -data=${cfg.databaseDir} \
681               ${escapeShellArgs cfg.extraFlags}
682           '';
683           MemoryDenyWriteExecute = true;
684           NoNewPrivileges = true;
685           PrivateDevices = true;
686           PrivateMounts = true;
687           PrivateTmp = true;
688           PrivateUsers = true;
689           ProtectControlGroups = true;
690           ProtectHostname = true;
691           ProtectKernelModules = true;
692           ProtectKernelTunables = true;
693           RestrictNamespaces = true;
694           RestrictRealtime = true;
695           RestrictSUIDSGID = true;
696           CapabilityBoundingSet = [
697             "~CAP_SYS_PTRACE" "~CAP_SYS_ADMIN"
698             "~CAP_SETGID" "~CAP_SETUID" "~CAP_SETPCAP"
699             "~CAP_SYS_TIME" "~CAP_KILL"
700           ];
701         };
702       };
703       syncthing-init = mkIf (cleanedConfig != {}) {
704         description = "Syncthing configuration updater";
705         requisite = [ "syncthing.service" ];
706         after = [ "syncthing.service" ];
707         wantedBy = [ "multi-user.target" ];
709         serviceConfig = {
710           User = cfg.user;
711           RemainAfterExit = true;
712           RuntimeDirectory = "syncthing-init";
713           Type = "oneshot";
714           ExecStart = updateConfig;
715         };
716       };
717     };
718   };