at 23.11-pre 22 kB view raw
1{ config, lib, options, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.syncthing; 7 opt = options.services.syncthing; 8 defaultUser = "syncthing"; 9 defaultGroup = defaultUser; 10 11 devices = mapAttrsToList (name: device: { 12 deviceID = device.id; 13 inherit (device) name addresses introducer autoAcceptFolders; 14 }) cfg.devices; 15 16 folders = mapAttrsToList ( _: folder: { 17 inherit (folder) path id label type; 18 devices = map (device: { deviceId = cfg.devices.${device}.id; }) folder.devices; 19 rescanIntervalS = folder.rescanInterval; 20 fsWatcherEnabled = folder.watch; 21 fsWatcherDelayS = folder.watchDelay; 22 ignorePerms = folder.ignorePerms; 23 ignoreDelete = folder.ignoreDelete; 24 versioning = folder.versioning; 25 }) (filterAttrs ( 26 _: folder: 27 folder.enable 28 ) cfg.folders); 29 30 updateConfig = pkgs.writers.writeDash "merge-syncthing-config" '' 31 set -efu 32 33 # be careful not to leak secrets in the filesystem or in process listings 34 35 umask 0077 36 37 # get the api key by parsing the config.xml 38 while 39 ! ${pkgs.libxml2}/bin/xmllint \ 40 --xpath 'string(configuration/gui/apikey)' \ 41 ${cfg.configDir}/config.xml \ 42 >"$RUNTIME_DIRECTORY/api_key" 43 do sleep 1; done 44 45 (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers" 46 47 curl() { 48 ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \ 49 --retry 1000 --retry-delay 1 --retry-all-errors \ 50 "$@" 51 } 52 53 # query the old config 54 old_cfg=$(curl ${cfg.guiAddress}/rest/config) 55 56 # generate the new config by merging with the NixOS config options 57 new_cfg=$(printf '%s\n' "$old_cfg" | ${pkgs.jq}/bin/jq -c '. * { 58 "devices": (${builtins.toJSON devices}${optionalString (cfg.devices == {} || ! cfg.overrideDevices) " + .devices"}), 59 "folders": (${builtins.toJSON folders}${optionalString (cfg.folders == {} || ! cfg.overrideFolders) " + .folders"}) 60 } * ${builtins.toJSON cfg.extraOptions}') 61 62 # send the new config 63 curl -X PUT -d "$new_cfg" ${cfg.guiAddress}/rest/config 64 65 # restart Syncthing if required 66 if curl ${cfg.guiAddress}/rest/config/restart-required | 67 ${pkgs.jq}/bin/jq -e .requiresRestart > /dev/null; then 68 curl -X POST ${cfg.guiAddress}/rest/system/restart 69 fi 70 ''; 71in { 72 ###### interface 73 options = { 74 services.syncthing = { 75 76 enable = mkEnableOption 77 (lib.mdDoc "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync"); 78 79 cert = mkOption { 80 type = types.nullOr types.str; 81 default = null; 82 description = mdDoc '' 83 Path to the `cert.pem` file, which will be copied into Syncthing's 84 [configDir](#opt-services.syncthing.configDir). 85 ''; 86 }; 87 88 key = mkOption { 89 type = types.nullOr types.str; 90 default = null; 91 description = mdDoc '' 92 Path to the `key.pem` file, which will be copied into Syncthing's 93 [configDir](#opt-services.syncthing.configDir). 94 ''; 95 }; 96 97 overrideDevices = mkOption { 98 type = types.bool; 99 default = true; 100 description = mdDoc '' 101 Whether to delete the devices which are not configured via the 102 [devices](#opt-services.syncthing.devices) option. 103 If set to `false`, devices added via the web 104 interface will persist and will have to be deleted manually. 105 ''; 106 }; 107 108 devices = mkOption { 109 default = {}; 110 description = mdDoc '' 111 Peers/devices which Syncthing should communicate with. 112 113 Note that you can still add devices manually, but those changes 114 will be reverted on restart if [overrideDevices](#opt-services.syncthing.overrideDevices) 115 is enabled. 116 ''; 117 example = { 118 bigbox = { 119 id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU"; 120 addresses = [ "tcp://192.168.0.10:51820" ]; 121 }; 122 }; 123 type = types.attrsOf (types.submodule ({ name, ... }: { 124 options = { 125 126 name = mkOption { 127 type = types.str; 128 default = name; 129 description = lib.mdDoc '' 130 The name of the device. 131 ''; 132 }; 133 134 addresses = mkOption { 135 type = types.listOf types.str; 136 default = []; 137 description = lib.mdDoc '' 138 The addresses used to connect to the device. 139 If this is left empty, dynamic configuration is attempted. 140 ''; 141 }; 142 143 id = mkOption { 144 type = types.str; 145 description = mdDoc '' 146 The device ID. See <https://docs.syncthing.net/dev/device-ids.html>. 147 ''; 148 }; 149 150 introducer = mkOption { 151 type = types.bool; 152 default = false; 153 description = mdDoc '' 154 Whether the device should act as an introducer and be allowed 155 to add folders on this computer. 156 See <https://docs.syncthing.net/users/introducer.html>. 157 ''; 158 }; 159 160 autoAcceptFolders = mkOption { 161 type = types.bool; 162 default = false; 163 description = mdDoc '' 164 Automatically create or share folders that this device advertises at the default path. 165 See <https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format>. 166 ''; 167 }; 168 169 }; 170 })); 171 }; 172 173 overrideFolders = mkOption { 174 type = types.bool; 175 default = true; 176 description = mdDoc '' 177 Whether to delete the folders which are not configured via the 178 [folders](#opt-services.syncthing.folders) option. 179 If set to `false`, folders added via the web 180 interface will persist and will have to be deleted manually. 181 ''; 182 }; 183 184 folders = mkOption { 185 default = {}; 186 description = mdDoc '' 187 Folders which should be shared by Syncthing. 188 189 Note that you can still add folders manually, but those changes 190 will be reverted on restart if [overrideFolders](#opt-services.syncthing.overrideFolders) 191 is enabled. 192 ''; 193 example = literalExpression '' 194 { 195 "/home/user/sync" = { 196 id = "syncme"; 197 devices = [ "bigbox" ]; 198 }; 199 } 200 ''; 201 type = types.attrsOf (types.submodule ({ name, ... }: { 202 options = { 203 204 enable = mkOption { 205 type = types.bool; 206 default = true; 207 description = lib.mdDoc '' 208 Whether to share this folder. 209 This option is useful when you want to define all folders 210 in one place, but not every machine should share all folders. 211 ''; 212 }; 213 214 path = mkOption { 215 # TODO for release 23.05: allow relative paths again and set 216 # working directory to cfg.dataDir 217 type = types.str // { 218 check = x: types.str.check x && (substring 0 1 x == "/" || substring 0 2 x == "~/"); 219 description = types.str.description + " starting with / or ~/"; 220 }; 221 default = name; 222 description = lib.mdDoc '' 223 The path to the folder which should be shared. 224 Only absolute paths (starting with `/`) and paths relative to 225 the [user](#opt-services.syncthing.user)'s home directory 226 (starting with `~/`) are allowed. 227 ''; 228 }; 229 230 id = mkOption { 231 type = types.str; 232 default = name; 233 description = lib.mdDoc '' 234 The ID of the folder. Must be the same on all devices. 235 ''; 236 }; 237 238 label = mkOption { 239 type = types.str; 240 default = name; 241 description = lib.mdDoc '' 242 The label of the folder. 243 ''; 244 }; 245 246 devices = mkOption { 247 type = types.listOf types.str; 248 default = []; 249 description = mdDoc '' 250 The devices this folder should be shared with. Each device must 251 be defined in the [devices](#opt-services.syncthing.devices) option. 252 ''; 253 }; 254 255 versioning = mkOption { 256 default = null; 257 description = mdDoc '' 258 How to keep changed/deleted files with Syncthing. 259 There are 4 different types of versioning with different parameters. 260 See <https://docs.syncthing.net/users/versioning.html>. 261 ''; 262 example = literalExpression '' 263 [ 264 { 265 versioning = { 266 type = "simple"; 267 params.keep = "10"; 268 }; 269 } 270 { 271 versioning = { 272 type = "trashcan"; 273 params.cleanoutDays = "1000"; 274 }; 275 } 276 { 277 versioning = { 278 type = "staggered"; 279 fsPath = "/syncthing/backup"; 280 params = { 281 cleanInterval = "3600"; 282 maxAge = "31536000"; 283 }; 284 }; 285 } 286 { 287 versioning = { 288 type = "external"; 289 params.versionsPath = pkgs.writers.writeBash "backup" ''' 290 folderpath="$1" 291 filepath="$2" 292 rm -rf "$folderpath/$filepath" 293 '''; 294 }; 295 } 296 ] 297 ''; 298 type = with types; nullOr (submodule { 299 options = { 300 type = mkOption { 301 type = enum [ "external" "simple" "staggered" "trashcan" ]; 302 description = mdDoc '' 303 The type of versioning. 304 See <https://docs.syncthing.net/users/versioning.html>. 305 ''; 306 }; 307 fsPath = mkOption { 308 default = ""; 309 type = either str path; 310 description = mdDoc '' 311 Path to the versioning folder. 312 See <https://docs.syncthing.net/users/versioning.html>. 313 ''; 314 }; 315 params = mkOption { 316 type = attrsOf (either str path); 317 description = mdDoc '' 318 The parameters for versioning. Structure depends on 319 [versioning.type](#opt-services.syncthing.folders._name_.versioning.type). 320 See <https://docs.syncthing.net/users/versioning.html>. 321 ''; 322 }; 323 }; 324 }); 325 }; 326 327 rescanInterval = mkOption { 328 type = types.int; 329 default = 3600; 330 description = lib.mdDoc '' 331 How often the folder should be rescanned for changes. 332 ''; 333 }; 334 335 type = mkOption { 336 type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ]; 337 default = "sendreceive"; 338 description = lib.mdDoc '' 339 Whether to only send changes for this folder, only receive them 340 or both. `receiveencrypted` can be used for untrusted devices. See 341 <https://docs.syncthing.net/users/untrusted.html> for reference. 342 ''; 343 }; 344 345 watch = mkOption { 346 type = types.bool; 347 default = true; 348 description = lib.mdDoc '' 349 Whether the folder should be watched for changes by inotify. 350 ''; 351 }; 352 353 watchDelay = mkOption { 354 type = types.int; 355 default = 10; 356 description = lib.mdDoc '' 357 The delay after an inotify event is triggered. 358 ''; 359 }; 360 361 ignorePerms = mkOption { 362 type = types.bool; 363 default = true; 364 description = lib.mdDoc '' 365 Whether to ignore permission changes. 366 ''; 367 }; 368 369 ignoreDelete = mkOption { 370 type = types.bool; 371 default = false; 372 description = mdDoc '' 373 Whether to skip deleting files that are deleted by peers. 374 See <https://docs.syncthing.net/advanced/folder-ignoredelete.html>. 375 ''; 376 }; 377 }; 378 })); 379 }; 380 381 extraOptions = mkOption { 382 type = types.addCheck (pkgs.formats.json {}).type isAttrs; 383 default = {}; 384 description = mdDoc '' 385 Extra configuration options for Syncthing. 386 See <https://docs.syncthing.net/users/config.html>. 387 Note that this attribute set does not exactly match the documented 388 xml format. Instead, this is the format of the json rest api. There 389 are slight differences. For example, this xml: 390 ```xml 391 <options> 392 <listenAddress>default</listenAddress> 393 <minHomeDiskFree unit="%">1</minHomeDiskFree> 394 </options> 395 ``` 396 corresponds to the json: 397 ```json 398 { 399 options: { 400 listenAddresses = [ 401 "default" 402 ]; 403 minHomeDiskFree = { 404 unit = "%"; 405 value = 1; 406 }; 407 }; 408 } 409 ``` 410 ''; 411 example = { 412 options.localAnnounceEnabled = false; 413 gui.theme = "black"; 414 }; 415 }; 416 417 guiAddress = mkOption { 418 type = types.str; 419 default = "127.0.0.1:8384"; 420 description = lib.mdDoc '' 421 The address to serve the web interface at. 422 ''; 423 }; 424 425 systemService = mkOption { 426 type = types.bool; 427 default = true; 428 description = lib.mdDoc '' 429 Whether to auto-launch Syncthing as a system service. 430 ''; 431 }; 432 433 user = mkOption { 434 type = types.str; 435 default = defaultUser; 436 example = "yourUser"; 437 description = mdDoc '' 438 The user to run Syncthing as. 439 By default, a user named `${defaultUser}` will be created whose home 440 directory is [dataDir](#opt-services.syncthing.dataDir). 441 ''; 442 }; 443 444 group = mkOption { 445 type = types.str; 446 default = defaultGroup; 447 example = "yourGroup"; 448 description = mdDoc '' 449 The group to run Syncthing under. 450 By default, a group named `${defaultGroup}` will be created. 451 ''; 452 }; 453 454 all_proxy = mkOption { 455 type = with types; nullOr str; 456 default = null; 457 example = "socks5://address.com:1234"; 458 description = mdDoc '' 459 Overwrites the all_proxy environment variable for the Syncthing process to 460 the given value. This is normally used to let Syncthing connect 461 through a SOCKS5 proxy server. 462 See <https://docs.syncthing.net/users/proxying.html>. 463 ''; 464 }; 465 466 dataDir = mkOption { 467 type = types.path; 468 default = "/var/lib/syncthing"; 469 example = "/home/yourUser"; 470 description = lib.mdDoc '' 471 The path where synchronised directories will exist. 472 ''; 473 }; 474 475 configDir = let 476 cond = versionAtLeast config.system.stateVersion "19.03"; 477 in mkOption { 478 type = types.path; 479 description = lib.mdDoc '' 480 The path where the settings and keys will exist. 481 ''; 482 default = cfg.dataDir + optionalString cond "/.config/syncthing"; 483 defaultText = literalMD '' 484 * if `stateVersion >= 19.03`: 485 486 config.${opt.dataDir} + "/.config/syncthing" 487 * otherwise: 488 489 config.${opt.dataDir} 490 ''; 491 }; 492 493 extraFlags = mkOption { 494 type = types.listOf types.str; 495 default = []; 496 example = [ "--reset-deltas" ]; 497 description = lib.mdDoc '' 498 Extra flags passed to the syncthing command in the service definition. 499 ''; 500 }; 501 502 openDefaultPorts = mkOption { 503 type = types.bool; 504 default = false; 505 example = true; 506 description = lib.mdDoc '' 507 Whether to open the default ports in the firewall: TCP/UDP 22000 for transfers 508 and UDP 21027 for discovery. 509 510 If multiple users are running Syncthing on this machine, you will need 511 to manually open a set of ports for each instance and leave this disabled. 512 Alternatively, if you are running only a single instance on this machine 513 using the default ports, enable this. 514 ''; 515 }; 516 517 package = mkOption { 518 type = types.package; 519 default = pkgs.syncthing; 520 defaultText = literalExpression "pkgs.syncthing"; 521 description = lib.mdDoc '' 522 The Syncthing package to use. 523 ''; 524 }; 525 }; 526 }; 527 528 imports = [ 529 (mkRemovedOptionModule [ "services" "syncthing" "useInotify" ] '' 530 This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher". 531 It can be enabled on a per-folder basis through the web interface. 532 '') 533 ] ++ map (o: 534 mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ] 535 ) [ "cert" "key" "devices" "folders" "overrideDevices" "overrideFolders" "extraOptions"]; 536 537 ###### implementation 538 539 config = mkIf cfg.enable { 540 541 networking.firewall = mkIf cfg.openDefaultPorts { 542 allowedTCPPorts = [ 22000 ]; 543 allowedUDPPorts = [ 21027 22000 ]; 544 }; 545 546 systemd.packages = [ pkgs.syncthing ]; 547 548 users.users = mkIf (cfg.systemService && cfg.user == defaultUser) { 549 ${defaultUser} = 550 { group = cfg.group; 551 home = cfg.dataDir; 552 createHome = true; 553 uid = config.ids.uids.syncthing; 554 description = "Syncthing daemon user"; 555 }; 556 }; 557 558 users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) { 559 ${defaultGroup}.gid = 560 config.ids.gids.syncthing; 561 }; 562 563 systemd.services = { 564 # upstream reference: 565 # https://github.com/syncthing/syncthing/blob/main/etc/linux-systemd/system/syncthing%40.service 566 syncthing = mkIf cfg.systemService { 567 description = "Syncthing service"; 568 after = [ "network.target" ]; 569 environment = { 570 STNORESTART = "yes"; 571 STNOUPGRADE = "yes"; 572 inherit (cfg) all_proxy; 573 } // config.networking.proxy.envVars; 574 wantedBy = [ "multi-user.target" ]; 575 serviceConfig = { 576 Restart = "on-failure"; 577 SuccessExitStatus = "3 4"; 578 RestartForceExitStatus="3 4"; 579 User = cfg.user; 580 Group = cfg.group; 581 ExecStartPre = mkIf (cfg.cert != null || cfg.key != null) 582 "+${pkgs.writers.writeBash "syncthing-copy-keys" '' 583 install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir} 584 ${optionalString (cfg.cert != null) '' 585 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.cert} ${cfg.configDir}/cert.pem 586 ''} 587 ${optionalString (cfg.key != null) '' 588 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem 589 ''} 590 ''}" 591 ; 592 ExecStart = '' 593 ${cfg.package}/bin/syncthing \ 594 -no-browser \ 595 -gui-address=${cfg.guiAddress} \ 596 -home=${cfg.configDir} ${escapeShellArgs cfg.extraFlags} 597 ''; 598 MemoryDenyWriteExecute = true; 599 NoNewPrivileges = true; 600 PrivateDevices = true; 601 PrivateMounts = true; 602 PrivateTmp = true; 603 PrivateUsers = true; 604 ProtectControlGroups = true; 605 ProtectHostname = true; 606 ProtectKernelModules = true; 607 ProtectKernelTunables = true; 608 RestrictNamespaces = true; 609 RestrictRealtime = true; 610 RestrictSUIDSGID = true; 611 CapabilityBoundingSet = [ 612 "~CAP_SYS_PTRACE" "~CAP_SYS_ADMIN" 613 "~CAP_SETGID" "~CAP_SETUID" "~CAP_SETPCAP" 614 "~CAP_SYS_TIME" "~CAP_KILL" 615 ]; 616 }; 617 }; 618 syncthing-init = mkIf ( 619 cfg.devices != {} || cfg.folders != {} || cfg.extraOptions != {} 620 ) { 621 description = "Syncthing configuration updater"; 622 requisite = [ "syncthing.service" ]; 623 after = [ "syncthing.service" ]; 624 wantedBy = [ "multi-user.target" ]; 625 626 serviceConfig = { 627 User = cfg.user; 628 RemainAfterExit = true; 629 RuntimeDirectory = "syncthing-init"; 630 Type = "oneshot"; 631 ExecStart = updateConfig; 632 }; 633 }; 634 635 syncthing-resume = { 636 wantedBy = [ "suspend.target" ]; 637 }; 638 }; 639 }; 640}