1 { config, lib, options, pkgs, ... }:
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
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}"
28 devices = mapAttrsToList (_: device: device // {
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.
37 devices = map (device:
38 if builtins.isString device then
39 { deviceId = cfg.settings.devices.${device}.id; }
43 }) (filterAttrs (_: folder:
45 ) cfg.settings.folders);
47 jq = "${pkgs.jq}/bin/jq";
48 updateConfig = pkgs.writers.writeBash "merge-syncthing-config" (''
51 # be careful not to leak secrets in the filesystem or in process listings
55 # get the api key by parsing the config.xml
57 ! ${pkgs.libxml2}/bin/xmllint \
58 --xpath 'string(configuration/gui/apikey)' \
59 ${cfg.configDir}/config.xml \
60 >"$RUNTIME_DIRECTORY/api_key"
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 \
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
73 # The attributes below are the only ones that are different for devices /
76 new_conf_IDs = map (v: v.id) devices;
77 GET_IdAttrName = "deviceID";
78 override = cfg.overrideDevices;
80 baseAddress = curlAddressArgs "/rest/config/devices";
83 new_conf_IDs = map (v: v.id) folders;
84 GET_IdAttrName = "id";
85 override = cfg.overrideFolders;
87 baseAddress = curlAddressArgs "/rest/config/folders";
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.
96 # Quoting https://docs.syncthing.net/rest/config.html:
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
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
107 curl -d ${lib.escapeShellArg (builtins.toJSON new_cfg)} -X POST ${s.baseAddress}
109 (lib.concatStringsSep "\n")
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
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)} \
119 '[.[].${s.GET_IdAttrName}] - $new_ids | .[]'
121 for id in ''${stale_${conf_type}_ids}; do
122 curl -X DELETE ${s.baseAddress}/$id
127 (lib.concatStringsSep "\n")
129 /* Now we update the other settings defined in cleanedConfig which are not
130 "folders" or "devices". */
131 (lib.pipe cleanedConfig [
133 (lib.subtractLists ["folders" "devices"])
135 curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"}
137 (lib.concatStringsSep "\n")
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"}
148 services.syncthing = {
150 enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
153 type = types.nullOr types.str;
156 Path to the `cert.pem` file, which will be copied into Syncthing's
157 [configDir](#opt-services.syncthing.configDir).
162 type = types.nullOr types.str;
165 Path to the `key.pem` file, which will be copied into Syncthing's
166 [configDir](#opt-services.syncthing.configDir).
170 overrideDevices = mkOption {
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.
181 overrideFolders = mkOption {
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.
192 settings = mkOption {
193 type = types.submodule {
194 freeformType = settingsFormat.type;
200 The options element contains all other global configuration options
202 type = types.submodule ({ name, ... }: {
203 freeformType = settingsFormat.type;
205 localAnnounceEnabled = mkOption {
206 type = types.nullOr types.bool;
209 Whether to send announcements to the local LAN, also use such announcements to find other devices.
213 localAnnouncePort = mkOption {
214 type = types.nullOr types.int;
217 The port on which to listen and send IPv4 broadcast announcements to.
221 relaysEnabled = mkOption {
222 type = types.nullOr types.bool;
225 When true, relays will be connected to and potentially used for device to device connections.
229 urAccepted = mkOption {
230 type = types.nullOr types.int;
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.
239 limitBandwidthInLan = mkOption {
240 type = types.nullOr types.bool;
243 Whether to apply bandwidth limits to devices in the same broadcast domain as the local device.
247 maxFolderConcurrency = mkOption {
248 type = types.nullOr types.int;
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).
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)
271 id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
272 addresses = [ "tcp://192.168.0.10:51820" ];
275 type = types.attrsOf (types.submodule ({ name, ... }: {
276 freeformType = settingsFormat.type;
283 The name of the device.
290 The device ID. See <https://docs.syncthing.net/dev/device-ids.html>.
294 autoAcceptFolders = mkOption {
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>.
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)
317 example = literalExpression ''
319 "/home/user/sync" = {
321 devices = [ "bigbox" ];
325 type = types.attrsOf (types.submodule ({ name, ... }: {
326 freeformType = settingsFormat.type;
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.
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 ~/";
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.
359 The ID of the folder. Must be the same on all devices.
367 The label of the folder.
372 type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ];
373 default = "sendreceive";
375 Controls how the folder is handled by Syncthing.
376 See <https://docs.syncthing.net/users/config.html#config-option-folder.type>.
381 type = types.listOf types.str;
384 The devices this folder should be shared with. Each device must
385 be defined in the [devices](#opt-services.syncthing.settings.devices) option.
389 versioning = mkOption {
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>.
396 example = literalExpression ''
407 params.cleanoutDays = "1000";
413 fsPath = "/syncthing/backup";
415 cleanInterval = "3600";
423 params.versionsPath = pkgs.writers.writeBash "backup" '''
426 rm -rf "$folderpath/$filepath"
432 type = with types; nullOr (submodule {
433 freeformType = settingsFormat.type;
436 type = enum [ "external" "simple" "staggered" "trashcan" ];
438 The type of versioning.
439 See <https://docs.syncthing.net/users/versioning.html>.
446 copyOwnershipFromParent = mkOption {
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).
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:
469 <listenAddress>default</listenAddress>
470 <minHomeDiskFree unit="%">1</minHomeDiskFree>
473 corresponds to the json:
489 options.localAnnounceEnabled = false;
494 guiAddress = mkOption {
496 default = "127.0.0.1:8384";
498 The address to serve the web interface at.
502 systemService = mkOption {
506 Whether to auto-launch Syncthing as a system service.
512 default = defaultUser;
513 example = "yourUser";
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).
523 default = defaultGroup;
524 example = "yourGroup";
526 The group to run Syncthing under.
527 By default, a group named `${defaultGroup}` will be created.
531 all_proxy = mkOption {
532 type = with types; nullOr str;
534 example = "socks5://address.com:1234";
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>.
545 default = "/var/lib/syncthing";
546 example = "/home/yourUser";
548 The path where synchronised directories will exist.
553 cond = versionAtLeast config.system.stateVersion "19.03";
557 The path where the settings and keys will exist.
559 default = cfg.dataDir + optionalString cond "/.config/syncthing";
560 defaultText = literalMD ''
561 * if `stateVersion >= 19.03`:
563 config.${opt.dataDir} + "/.config/syncthing"
566 config.${opt.dataDir}
570 databaseDir = mkOption {
573 The directory containing the database and logs.
575 default = cfg.configDir;
576 defaultText = literalExpression "config.${opt.configDir}";
579 extraFlags = mkOption {
580 type = types.listOf types.str;
582 example = [ "--reset-deltas" ];
584 Extra flags passed to the syncthing command in the service definition.
588 openDefaultPorts = mkOption {
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.
603 package = mkPackageOption pkgs "syncthing" { };
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.
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" ])
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 ];
629 systemd.packages = [ pkgs.syncthing ];
631 users.users = mkIf (cfg.systemService && cfg.user == defaultUser) {
636 uid = config.ids.uids.syncthing;
637 description = "Syncthing daemon user";
641 users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) {
642 ${defaultGroup}.gid =
643 config.ids.gids.syncthing;
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" ];
655 inherit (cfg) all_proxy;
656 } // config.networking.proxy.envVars;
657 wantedBy = [ "multi-user.target" ];
659 Restart = "on-failure";
660 SuccessExitStatus = "3 4";
661 RestartForceExitStatus="3 4";
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
670 ${optionalString (cfg.key != null) ''
671 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem
676 ${cfg.package}/bin/syncthing \
678 -gui-address=${if isUnixGui then "unix://" else ""}${cfg.guiAddress} \
679 -config=${cfg.configDir} \
680 -data=${cfg.databaseDir} \
681 ${escapeShellArgs cfg.extraFlags}
683 MemoryDenyWriteExecute = true;
684 NoNewPrivileges = true;
685 PrivateDevices = true;
686 PrivateMounts = 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"
703 syncthing-init = mkIf (cleanedConfig != {}) {
704 description = "Syncthing configuration updater";
705 requisite = [ "syncthing.service" ];
706 after = [ "syncthing.service" ];
707 wantedBy = [ "multi-user.target" ];
711 RemainAfterExit = true;
712 RuntimeDirectory = "syncthing-init";
714 ExecStart = updateConfig;