1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}:
8
9with lib;
10
11let
12 cfg = config.services.syncthing;
13 opt = options.services.syncthing;
14 defaultUser = "syncthing";
15 defaultGroup = defaultUser;
16 settingsFormat = pkgs.formats.json { };
17 cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != { })) cfg.settings;
18
19 isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/";
20
21 # Syncthing supports serving the GUI over Unix sockets. If that happens, the
22 # API is served over the Unix socket as well. This function returns the correct
23 # curl arguments for the address portion of the curl command for both network
24 # and Unix socket addresses.
25 curlAddressArgs =
26 path:
27 if
28 isUnixGui
29 # if cfg.guiAddress is a unix socket, tell curl explicitly about it
30 # note that the dot in front of `${path}` is the hostname, which is
31 # required.
32 then
33 "--unix-socket ${cfg.guiAddress} http://.${path}"
34 # no adjustments are needed if cfg.guiAddress is a network address
35 else
36 "${cfg.guiAddress}${path}";
37
38 devices = mapAttrsToList (
39 _: device:
40 device
41 // {
42 deviceID = device.id;
43 }
44 ) cfg.settings.devices;
45
46 anyAutoAccept = builtins.any (dev: dev.autoAcceptFolders) devices;
47
48 folders = mapAttrsToList (
49 _: folder:
50 folder
51 //
52 throwIf (folder ? rescanInterval || folder ? watch || folder ? watchDelay)
53 ''
54 The options services.syncthing.settings.folders.<name>.{rescanInterval,watch,watchDelay}
55 were removed. Please use, respectively, {rescanIntervalS,fsWatcherEnabled,fsWatcherDelayS} instead.
56 ''
57 {
58 devices =
59 let
60 folderDevices = folder.devices;
61 in
62 map (
63 device:
64 if builtins.isString device then
65 { deviceId = cfg.settings.devices.${device}.id; }
66 else if builtins.isAttrs device then
67 { deviceId = cfg.settings.devices.${device.name}.id; } // device
68 else
69 throw "Invalid type for devices in folder '${folderName}'; expected list or attrset."
70 ) folderDevices;
71 }
72 ) (filterAttrs (_: folder: folder.enable) cfg.settings.folders);
73
74 jq = "${pkgs.jq}/bin/jq";
75 updateConfig = pkgs.writers.writeBash "merge-syncthing-config" (
76 ''
77 set -efu
78
79 # be careful not to leak secrets in the filesystem or in process listings
80 umask 0077
81
82 curl() {
83 # get the api key by parsing the config.xml
84 while
85 ! ${pkgs.libxml2}/bin/xmllint \
86 --xpath 'string(configuration/gui/apikey)' \
87 ${cfg.configDir}/config.xml \
88 >"$RUNTIME_DIRECTORY/api_key"
89 do sleep 1; done
90 (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
91 ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
92 --retry 1000 --retry-delay 1 --retry-all-errors \
93 "$@"
94 }
95 ''
96 +
97
98 /*
99 Syncthing's rest API for the folders and devices is almost identical.
100 Hence we iterate them using lib.pipe and generate shell commands for both at
101 the same time.
102 */
103 (lib.pipe
104 {
105 # The attributes below are the only ones that are different for devices /
106 # folders.
107 devs = {
108 new_conf_IDs = map (v: v.id) devices;
109 GET_IdAttrName = "deviceID";
110 override = cfg.overrideDevices;
111 conf = devices;
112 baseAddress = curlAddressArgs "/rest/config/devices";
113 };
114 dirs = {
115 new_conf_IDs = map (v: v.id) folders;
116 GET_IdAttrName = "id";
117 override = cfg.overrideFolders;
118 conf = folders;
119 baseAddress = curlAddressArgs "/rest/config/folders";
120 };
121 }
122 [
123 # Now for each of these attributes, write the curl commands that are
124 # identical to both folders and devices.
125 (mapAttrs (
126 conf_type: s:
127 # We iterate the `conf` list now, and run a curl -X POST command for each, that
128 # should update that device/folder only.
129 lib.pipe s.conf [
130 # Quoting https://docs.syncthing.net/rest/config.html:
131 #
132 # > PUT takes an array and POST a single object. In both cases if a
133 # given folder/device already exists, it’s replaced, otherwise a new
134 # one is added.
135 #
136 # What's not documented, is that using PUT will remove objects that
137 # don't exist in the array given. That's why we use here `POST`, and
138 # only if s.override == true then we DELETE the relevant folders
139 # afterwards.
140 (map (
141 new_cfg:
142 let
143 jsonPreSecretsFile = pkgs.writeTextFile {
144 name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json";
145 text = builtins.toJSON new_cfg;
146 };
147 injectSecretsJqCmd =
148 {
149 # There are no secrets in `devs`, so no massaging needed.
150 "devs" = "${jq} .";
151 "dirs" =
152 let
153 folder = new_cfg;
154 devicesWithSecrets = lib.pipe folder.devices [
155 (lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile))
156 (map (device: {
157 deviceId = device.deviceId;
158 variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}";
159 secretPath = device.encryptionPasswordFile;
160 }))
161 ];
162 # At this point, `jsonPreSecretsFile` looks something like this:
163 #
164 # {
165 # ...,
166 # "devices": [
167 # {
168 # "deviceId": "id1",
169 # "encryptionPasswordFile": "/etc/bar-encryption-password",
170 # "name": "..."
171 # }
172 # ],
173 # }
174 #
175 # We now generate a `jq` command that can replace those
176 # `encryptionPasswordFile`s with `encryptionPassword`.
177 # The `jq` command ends up looking like this:
178 #
179 # jq --rawfile secret_DEADBEEF /etc/bar-encryption-password '
180 # .devices[] |= (
181 # if .deviceId == "id1" then
182 # del(.encryptionPasswordFile) |
183 # .encryptionPassword = $secret_DEADBEEF
184 # else
185 # .
186 # end
187 # )
188 # '
189 jqUpdates = map (device: ''
190 .devices[] |= (
191 if .deviceId == "${device.deviceId}" then
192 del(.encryptionPasswordFile) |
193 .encryptionPassword = ''$${device.variableName}
194 else
195 .
196 end
197 )
198 '') devicesWithSecrets;
199 jqRawFiles = map (
200 device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}"
201 ) devicesWithSecrets;
202 in
203 "${jq} ${lib.concatStringsSep " " jqRawFiles} ${
204 lib.escapeShellArg (lib.concatStringsSep "|" ([ "." ] ++ jqUpdates))
205 }";
206 }
207 .${conf_type};
208 in
209 ''
210 ${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress}
211 ''
212 ))
213 (lib.concatStringsSep "\n")
214 ]
215 /*
216 If we need to override devices/folders, we iterate all currently configured
217 IDs, via another `curl -X GET`, and we delete all IDs that are not part of
218 the Nix configured list of IDs
219 */
220 + lib.optionalString s.override ''
221 stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \
222 --argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \
223 --raw-output \
224 '[.[].${s.GET_IdAttrName}] - $new_ids | .[]'
225 )"
226 for id in ''${stale_${conf_type}_ids}; do
227 >&2 echo "Deleting stale device: $id"
228 curl -X DELETE ${s.baseAddress}/$id
229 done
230 ''
231 ))
232 builtins.attrValues
233 (lib.concatStringsSep "\n")
234 ]
235 )
236 +
237 /*
238 Now we update the other settings defined in cleanedConfig which are not
239 "folders" or "devices".
240 */
241 (lib.pipe cleanedConfig [
242 builtins.attrNames
243 (lib.subtractLists [
244 "folders"
245 "devices"
246 ])
247 (map (subOption: ''
248 curl -X PUT -d ${
249 lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})
250 } ${curlAddressArgs "/rest/config/${subOption}"}
251 ''))
252 (lib.concatStringsSep "\n")
253 ])
254 + ''
255 # restart Syncthing if required
256 if curl ${curlAddressArgs "/rest/config/restart-required"} |
257 ${jq} -e .requiresRestart > /dev/null; then
258 curl -X POST ${curlAddressArgs "/rest/system/restart"}
259 fi
260 ''
261 );
262in
263{
264 ###### interface
265 options = {
266 services.syncthing = {
267
268 enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
269
270 cert = mkOption {
271 type = types.nullOr types.str;
272 default = null;
273 description = ''
274 Path to the `cert.pem` file, which will be copied into Syncthing's
275 [configDir](#opt-services.syncthing.configDir).
276 '';
277 };
278
279 key = mkOption {
280 type = types.nullOr types.str;
281 default = null;
282 description = ''
283 Path to the `key.pem` file, which will be copied into Syncthing's
284 [configDir](#opt-services.syncthing.configDir).
285 '';
286 };
287
288 overrideDevices = mkOption {
289 type = types.bool;
290 default = true;
291 description = ''
292 Whether to delete the devices which are not configured via the
293 [devices](#opt-services.syncthing.settings.devices) option.
294 If set to `false`, devices added via the web
295 interface will persist and will have to be deleted manually.
296 '';
297 };
298
299 overrideFolders = mkOption {
300 type = types.bool;
301 default = !anyAutoAccept;
302 defaultText = literalMD ''
303 `true` unless any device has the
304 [autoAcceptFolders](#opt-services.syncthing.settings.devices._name_.autoAcceptFolders)
305 option set to `true`.
306 '';
307 description = ''
308 Whether to delete the folders which are not configured via the
309 [folders](#opt-services.syncthing.settings.folders) option.
310 If set to `false`, folders added via the web
311 interface will persist and will have to be deleted manually.
312 '';
313 };
314
315 settings = mkOption {
316 type = types.submodule {
317 freeformType = settingsFormat.type;
318 options = {
319 # global options
320 options = mkOption {
321 default = { };
322 description = ''
323 The options element contains all other global configuration options
324 '';
325 type = types.submodule (
326 { name, ... }:
327 {
328 freeformType = settingsFormat.type;
329 options = {
330 localAnnounceEnabled = mkOption {
331 type = types.nullOr types.bool;
332 default = null;
333 description = ''
334 Whether to send announcements to the local LAN, also use such announcements to find other devices.
335 '';
336 };
337
338 localAnnouncePort = mkOption {
339 type = types.nullOr types.int;
340 default = null;
341 description = ''
342 The port on which to listen and send IPv4 broadcast announcements to.
343 '';
344 };
345
346 relaysEnabled = mkOption {
347 type = types.nullOr types.bool;
348 default = null;
349 description = ''
350 When true, relays will be connected to and potentially used for device to device connections.
351 '';
352 };
353
354 urAccepted = mkOption {
355 type = types.nullOr types.int;
356 default = null;
357 description = ''
358 Whether the user has accepted to submit anonymous usage data.
359 The default, 0, mean the user has not made a choice, and Syncthing will ask at some point in the future.
360 "-1" means no, a number above zero means that that version of usage reporting has been accepted.
361 '';
362 };
363
364 limitBandwidthInLan = mkOption {
365 type = types.nullOr types.bool;
366 default = null;
367 description = ''
368 Whether to apply bandwidth limits to devices in the same broadcast domain as the local device.
369 '';
370 };
371
372 maxFolderConcurrency = mkOption {
373 type = types.nullOr types.int;
374 default = null;
375 description = ''
376 This option controls how many folders may concurrently be in I/O-intensive operations such as syncing or scanning.
377 The mechanism is described in detail in a [separate chapter](https://docs.syncthing.net/advanced/option-max-concurrency.html).
378 '';
379 };
380 };
381 }
382 );
383 };
384
385 # device settings
386 devices = mkOption {
387 default = { };
388 description = ''
389 Peers/devices which Syncthing should communicate with.
390
391 Note that you can still add devices manually, but those changes
392 will be reverted on restart if [overrideDevices](#opt-services.syncthing.overrideDevices)
393 is enabled.
394 '';
395 example = {
396 bigbox = {
397 id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
398 addresses = [ "tcp://192.168.0.10:51820" ];
399 };
400 };
401 type = types.attrsOf (
402 types.submodule (
403 { name, ... }:
404 {
405 freeformType = settingsFormat.type;
406 options = {
407
408 name = mkOption {
409 type = types.str;
410 default = name;
411 description = ''
412 The name of the device.
413 '';
414 };
415
416 id = mkOption {
417 type = types.str;
418 description = ''
419 The device ID. See <https://docs.syncthing.net/dev/device-ids.html>.
420 '';
421 };
422
423 autoAcceptFolders = mkOption {
424 type = types.bool;
425 default = false;
426 description = ''
427 Automatically create or share folders that this device advertises at the default path.
428 See <https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format>.
429 '';
430 };
431
432 };
433 }
434 )
435 );
436 };
437
438 # folder settings
439 folders = mkOption {
440 default = { };
441 description = ''
442 Folders which should be shared by Syncthing.
443
444 Note that you can still add folders manually, but those changes
445 will be reverted on restart if [overrideFolders](#opt-services.syncthing.overrideFolders)
446 is enabled.
447 '';
448 example = literalExpression ''
449 {
450 "/home/user/sync" = {
451 id = "syncme";
452 devices = [ "bigbox" ];
453 };
454 }
455 '';
456 type = types.attrsOf (
457 types.submodule (
458 { name, ... }:
459 {
460 freeformType = settingsFormat.type;
461 options = {
462
463 enable = mkOption {
464 type = types.bool;
465 default = true;
466 description = ''
467 Whether to share this folder.
468 This option is useful when you want to define all folders
469 in one place, but not every machine should share all folders.
470 '';
471 };
472
473 path = mkOption {
474 # TODO for release 23.05: allow relative paths again and set
475 # working directory to cfg.dataDir
476 type = types.str // {
477 check = x: types.str.check x && (substring 0 1 x == "/" || substring 0 2 x == "~/");
478 description = types.str.description + " starting with / or ~/";
479 };
480 default = name;
481 description = ''
482 The path to the folder which should be shared.
483 Only absolute paths (starting with `/`) and paths relative to
484 the [user](#opt-services.syncthing.user)'s home directory
485 (starting with `~/`) are allowed.
486 '';
487 };
488
489 id = mkOption {
490 type = types.str;
491 default = name;
492 description = ''
493 The ID of the folder. Must be the same on all devices.
494 '';
495 };
496
497 label = mkOption {
498 type = types.str;
499 default = name;
500 description = ''
501 The label of the folder.
502 '';
503 };
504
505 type = mkOption {
506 type = types.enum [
507 "sendreceive"
508 "sendonly"
509 "receiveonly"
510 "receiveencrypted"
511 ];
512 default = "sendreceive";
513 description = ''
514 Controls how the folder is handled by Syncthing.
515 See <https://docs.syncthing.net/users/config.html#config-option-folder.type>.
516 '';
517 };
518
519 devices = mkOption {
520 type = types.listOf (
521 types.oneOf [
522 types.str
523 (types.submodule (
524 { ... }:
525 {
526 freeformType = settingsFormat.type;
527 options = {
528 name = mkOption {
529 type = types.str;
530 default = null;
531 description = ''
532 The name of a device defined in the
533 [devices](#opt-services.syncthing.settings.devices)
534 option.
535 '';
536 };
537 encryptionPasswordFile = mkOption {
538 type = types.nullOr (
539 types.pathWith {
540 inStore = false;
541 absolute = true;
542 }
543 );
544 default = null;
545 description = ''
546 Path to encryption password. If set, the file will be read during
547 service activation, without being embedded in derivation.
548 '';
549 };
550 };
551 }
552 ))
553 ]
554 );
555 default = [ ];
556 description = ''
557 The devices this folder should be shared with. Each device must
558 be defined in the [devices](#opt-services.syncthing.settings.devices) option.
559
560 A list of either strings or attribute sets, where values
561 are device names or device configurations.
562 '';
563 };
564
565 versioning = mkOption {
566 default = null;
567 description = ''
568 How to keep changed/deleted files with Syncthing.
569 There are 4 different types of versioning with different parameters.
570 See <https://docs.syncthing.net/users/versioning.html>.
571 '';
572 example = literalExpression ''
573 [
574 {
575 versioning = {
576 type = "simple";
577 params.keep = "10";
578 };
579 }
580 {
581 versioning = {
582 type = "trashcan";
583 params.cleanoutDays = "1000";
584 };
585 }
586 {
587 versioning = {
588 type = "staggered";
589 fsPath = "/syncthing/backup";
590 params = {
591 cleanInterval = "3600";
592 maxAge = "31536000";
593 };
594 };
595 }
596 {
597 versioning = {
598 type = "external";
599 params.versionsPath = pkgs.writers.writeBash "backup" '''
600 folderpath="$1"
601 filepath="$2"
602 rm -rf "$folderpath/$filepath"
603 ''';
604 };
605 }
606 ]
607 '';
608 type =
609 with types;
610 nullOr (submodule {
611 freeformType = settingsFormat.type;
612 options = {
613 type = mkOption {
614 type = enum [
615 "external"
616 "simple"
617 "staggered"
618 "trashcan"
619 ];
620 description = ''
621 The type of versioning.
622 See <https://docs.syncthing.net/users/versioning.html>.
623 '';
624 };
625 };
626 });
627 };
628
629 copyOwnershipFromParent = mkOption {
630 type = types.bool;
631 default = false;
632 description = ''
633 On Unix systems, tries to copy file/folder ownership from the parent directory (the directory it’s located in).
634 Requires running Syncthing as a privileged user, or granting it additional capabilities (e.g. CAP_CHOWN on Linux).
635 '';
636 };
637 };
638 }
639 )
640 );
641 };
642
643 };
644 };
645 default = { };
646 description = ''
647 Extra configuration options for Syncthing.
648 See <https://docs.syncthing.net/users/config.html>.
649 Note that this attribute set does not exactly match the documented
650 xml format. Instead, this is the format of the json rest api. There
651 are slight differences. For example, this xml:
652 ```xml
653 <options>
654 <listenAddress>default</listenAddress>
655 <minHomeDiskFree unit="%">1</minHomeDiskFree>
656 </options>
657 ```
658 corresponds to the json:
659 ```json
660 {
661 options: {
662 listenAddresses = [
663 "default"
664 ];
665 minHomeDiskFree = {
666 unit = "%";
667 value = 1;
668 };
669 };
670 }
671 ```
672 '';
673 example = {
674 options.localAnnounceEnabled = false;
675 gui.theme = "black";
676 };
677 };
678
679 guiAddress = mkOption {
680 type = types.str;
681 default = "127.0.0.1:8384";
682 description = ''
683 The address to serve the web interface at.
684 '';
685 };
686
687 systemService = mkOption {
688 type = types.bool;
689 default = true;
690 description = ''
691 Whether to auto-launch Syncthing as a system service.
692 '';
693 };
694
695 user = mkOption {
696 type = types.str;
697 default = defaultUser;
698 example = "yourUser";
699 description = ''
700 The user to run Syncthing as.
701 By default, a user named `${defaultUser}` will be created whose home
702 directory is [dataDir](#opt-services.syncthing.dataDir).
703 '';
704 };
705
706 group = mkOption {
707 type = types.str;
708 default = defaultGroup;
709 example = "yourGroup";
710 description = ''
711 The group to run Syncthing under.
712 By default, a group named `${defaultGroup}` will be created.
713 '';
714 };
715
716 all_proxy = mkOption {
717 type = with types; nullOr str;
718 default = null;
719 example = "socks5://address.com:1234";
720 description = ''
721 Overwrites the all_proxy environment variable for the Syncthing process to
722 the given value. This is normally used to let Syncthing connect
723 through a SOCKS5 proxy server.
724 See <https://docs.syncthing.net/users/proxying.html>.
725 '';
726 };
727
728 dataDir = mkOption {
729 type = types.path;
730 default = "/var/lib/syncthing";
731 example = "/home/yourUser";
732 description = ''
733 The path where synchronised directories will exist.
734 '';
735 };
736
737 configDir =
738 let
739 cond = versionAtLeast config.system.stateVersion "19.03";
740 in
741 mkOption {
742 type = types.path;
743 description = ''
744 The path where the settings and keys will exist.
745 '';
746 default = cfg.dataDir + optionalString cond "/.config/syncthing";
747 defaultText = literalMD ''
748 * if `stateVersion >= 19.03`:
749
750 config.${opt.dataDir} + "/.config/syncthing"
751 * otherwise:
752
753 config.${opt.dataDir}
754 '';
755 };
756
757 databaseDir = mkOption {
758 type = types.path;
759 description = ''
760 The directory containing the database and logs.
761 '';
762 default = cfg.configDir;
763 defaultText = literalExpression "config.${opt.configDir}";
764 };
765
766 extraFlags = mkOption {
767 type = types.listOf types.str;
768 default = [ ];
769 example = [ "--reset-deltas" ];
770 description = ''
771 Extra flags passed to the syncthing command in the service definition.
772 '';
773 };
774
775 openDefaultPorts = mkOption {
776 type = types.bool;
777 default = false;
778 example = true;
779 description = ''
780 Whether to open the default ports in the firewall: TCP/UDP 22000 for transfers
781 and UDP 21027 for discovery.
782
783 If multiple users are running Syncthing on this machine, you will need
784 to manually open a set of ports for each instance and leave this disabled.
785 Alternatively, if you are running only a single instance on this machine
786 using the default ports, enable this.
787 '';
788 };
789
790 package = mkPackageOption pkgs "syncthing" { };
791 };
792 };
793
794 imports =
795 [
796 (mkRemovedOptionModule [ "services" "syncthing" "useInotify" ] ''
797 This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher".
798 It can be enabled on a per-folder basis through the web interface.
799 '')
800 (mkRenamedOptionModule
801 [ "services" "syncthing" "extraOptions" ]
802 [ "services" "syncthing" "settings" ]
803 )
804 (mkRenamedOptionModule
805 [ "services" "syncthing" "folders" ]
806 [ "services" "syncthing" "settings" "folders" ]
807 )
808 (mkRenamedOptionModule
809 [ "services" "syncthing" "devices" ]
810 [ "services" "syncthing" "settings" "devices" ]
811 )
812 (mkRenamedOptionModule
813 [ "services" "syncthing" "options" ]
814 [ "services" "syncthing" "settings" "options" ]
815 )
816 ]
817 ++ map
818 (o: mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ])
819 [
820 "cert"
821 "key"
822 "devices"
823 "folders"
824 "overrideDevices"
825 "overrideFolders"
826 "extraOptions"
827 ];
828
829 ###### implementation
830
831 config = mkIf cfg.enable {
832 assertions = [
833 {
834 assertion = !(cfg.overrideFolders && anyAutoAccept);
835 message = ''
836 services.syncthing.overrideFolders will delete auto-accepted folders
837 from the configuration, creating path conflicts.
838 '';
839 }
840 ];
841
842 networking.firewall = mkIf cfg.openDefaultPorts {
843 allowedTCPPorts = [ 22000 ];
844 allowedUDPPorts = [
845 21027
846 22000
847 ];
848 };
849
850 systemd.packages = [ pkgs.syncthing ];
851
852 users.users = mkIf (cfg.systemService && cfg.user == defaultUser) {
853 ${defaultUser} = {
854 group = cfg.group;
855 home = cfg.dataDir;
856 createHome = true;
857 uid = config.ids.uids.syncthing;
858 description = "Syncthing daemon user";
859 };
860 };
861
862 users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) {
863 ${defaultGroup}.gid = config.ids.gids.syncthing;
864 };
865
866 systemd.services = {
867 # upstream reference:
868 # https://github.com/syncthing/syncthing/blob/main/etc/linux-systemd/system/syncthing%40.service
869 syncthing = mkIf cfg.systemService {
870 description = "Syncthing service";
871 after = [ "network.target" ];
872 environment = {
873 STNORESTART = "yes";
874 STNOUPGRADE = "yes";
875 inherit (cfg) all_proxy;
876 } // config.networking.proxy.envVars;
877 wantedBy = [ "multi-user.target" ];
878 serviceConfig = {
879 Restart = "on-failure";
880 SuccessExitStatus = "3 4";
881 RestartForceExitStatus = "3 4";
882 User = cfg.user;
883 Group = cfg.group;
884 ExecStartPre =
885 mkIf (cfg.cert != null || cfg.key != null)
886 "+${pkgs.writers.writeBash "syncthing-copy-keys" ''
887 install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir}
888 ${optionalString (cfg.cert != null) ''
889 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.cert} ${cfg.configDir}/cert.pem
890 ''}
891 ${optionalString (cfg.key != null) ''
892 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem
893 ''}
894 ''}";
895 ExecStart = ''
896 ${cfg.package}/bin/syncthing \
897 -no-browser \
898 -gui-address=${if isUnixGui then "unix://" else ""}${cfg.guiAddress} \
899 -config=${cfg.configDir} \
900 -data=${cfg.databaseDir} \
901 ${escapeShellArgs cfg.extraFlags}
902 '';
903 MemoryDenyWriteExecute = true;
904 NoNewPrivileges = true;
905 PrivateDevices = true;
906 PrivateMounts = true;
907 PrivateTmp = true;
908 PrivateUsers = true;
909 ProtectControlGroups = true;
910 ProtectHostname = true;
911 ProtectKernelModules = true;
912 ProtectKernelTunables = true;
913 RestrictNamespaces = true;
914 RestrictRealtime = true;
915 RestrictSUIDSGID = true;
916 CapabilityBoundingSet = [
917 "~CAP_SYS_PTRACE"
918 "~CAP_SYS_ADMIN"
919 "~CAP_SETGID"
920 "~CAP_SETUID"
921 "~CAP_SETPCAP"
922 "~CAP_SYS_TIME"
923 "~CAP_KILL"
924 ];
925 };
926 };
927 syncthing-init = mkIf (cleanedConfig != { }) {
928 description = "Syncthing configuration updater";
929 requisite = [ "syncthing.service" ];
930 after = [ "syncthing.service" ];
931 wantedBy = [ "multi-user.target" ];
932
933 serviceConfig = {
934 User = cfg.user;
935 RemainAfterExit = true;
936 RuntimeDirectory = "syncthing-init";
937 Type = "oneshot";
938 ExecStart = updateConfig;
939 };
940 };
941 };
942 };
943}