at 25.11-pre 36 kB view raw
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 its 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}