at 23.05-pre 21 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 ''; 388 example = { 389 options.localAnnounceEnabled = false; 390 gui.theme = "black"; 391 }; 392 }; 393 394 guiAddress = mkOption { 395 type = types.str; 396 default = "127.0.0.1:8384"; 397 description = lib.mdDoc '' 398 The address to serve the web interface at. 399 ''; 400 }; 401 402 systemService = mkOption { 403 type = types.bool; 404 default = true; 405 description = lib.mdDoc '' 406 Whether to auto-launch Syncthing as a system service. 407 ''; 408 }; 409 410 user = mkOption { 411 type = types.str; 412 default = defaultUser; 413 example = "yourUser"; 414 description = mdDoc '' 415 The user to run Syncthing as. 416 By default, a user named `${defaultUser}` will be created whose home 417 directory is [dataDir](#opt-services.syncthing.dataDir). 418 ''; 419 }; 420 421 group = mkOption { 422 type = types.str; 423 default = defaultGroup; 424 example = "yourGroup"; 425 description = mdDoc '' 426 The group to run Syncthing under. 427 By default, a group named `${defaultGroup}` will be created. 428 ''; 429 }; 430 431 all_proxy = mkOption { 432 type = with types; nullOr str; 433 default = null; 434 example = "socks5://address.com:1234"; 435 description = mdDoc '' 436 Overwrites the all_proxy environment variable for the Syncthing process to 437 the given value. This is normally used to let Syncthing connect 438 through a SOCKS5 proxy server. 439 See <https://docs.syncthing.net/users/proxying.html>. 440 ''; 441 }; 442 443 dataDir = mkOption { 444 type = types.path; 445 default = "/var/lib/syncthing"; 446 example = "/home/yourUser"; 447 description = lib.mdDoc '' 448 The path where synchronised directories will exist. 449 ''; 450 }; 451 452 configDir = let 453 cond = versionAtLeast config.system.stateVersion "19.03"; 454 in mkOption { 455 type = types.path; 456 description = lib.mdDoc '' 457 The path where the settings and keys will exist. 458 ''; 459 default = cfg.dataDir + optionalString cond "/.config/syncthing"; 460 defaultText = literalMD '' 461 * if `stateVersion >= 19.03`: 462 463 config.${opt.dataDir} + "/.config/syncthing" 464 * otherwise: 465 466 config.${opt.dataDir} 467 ''; 468 }; 469 470 extraFlags = mkOption { 471 type = types.listOf types.str; 472 default = []; 473 example = [ "--reset-deltas" ]; 474 description = lib.mdDoc '' 475 Extra flags passed to the syncthing command in the service definition. 476 ''; 477 }; 478 479 openDefaultPorts = mkOption { 480 type = types.bool; 481 default = false; 482 example = true; 483 description = lib.mdDoc '' 484 Whether to open the default ports in the firewall: TCP/UDP 22000 for transfers 485 and UDP 21027 for discovery. 486 487 If multiple users are running Syncthing on this machine, you will need 488 to manually open a set of ports for each instance and leave this disabled. 489 Alternatively, if you are running only a single instance on this machine 490 using the default ports, enable this. 491 ''; 492 }; 493 494 package = mkOption { 495 type = types.package; 496 default = pkgs.syncthing; 497 defaultText = literalExpression "pkgs.syncthing"; 498 description = lib.mdDoc '' 499 The Syncthing package to use. 500 ''; 501 }; 502 }; 503 }; 504 505 imports = [ 506 (mkRemovedOptionModule [ "services" "syncthing" "useInotify" ] '' 507 This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher". 508 It can be enabled on a per-folder basis through the web interface. 509 '') 510 ] ++ map (o: 511 mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ] 512 ) [ "cert" "key" "devices" "folders" "overrideDevices" "overrideFolders" "extraOptions"]; 513 514 ###### implementation 515 516 config = mkIf cfg.enable { 517 518 networking.firewall = mkIf cfg.openDefaultPorts { 519 allowedTCPPorts = [ 22000 ]; 520 allowedUDPPorts = [ 21027 22000 ]; 521 }; 522 523 systemd.packages = [ pkgs.syncthing ]; 524 525 users.users = mkIf (cfg.systemService && cfg.user == defaultUser) { 526 ${defaultUser} = 527 { group = cfg.group; 528 home = cfg.dataDir; 529 createHome = true; 530 uid = config.ids.uids.syncthing; 531 description = "Syncthing daemon user"; 532 }; 533 }; 534 535 users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) { 536 ${defaultGroup}.gid = 537 config.ids.gids.syncthing; 538 }; 539 540 systemd.services = { 541 # upstream reference: 542 # https://github.com/syncthing/syncthing/blob/main/etc/linux-systemd/system/syncthing%40.service 543 syncthing = mkIf cfg.systemService { 544 description = "Syncthing service"; 545 after = [ "network.target" ]; 546 environment = { 547 STNORESTART = "yes"; 548 STNOUPGRADE = "yes"; 549 inherit (cfg) all_proxy; 550 } // config.networking.proxy.envVars; 551 wantedBy = [ "multi-user.target" ]; 552 serviceConfig = { 553 Restart = "on-failure"; 554 SuccessExitStatus = "3 4"; 555 RestartForceExitStatus="3 4"; 556 User = cfg.user; 557 Group = cfg.group; 558 ExecStartPre = mkIf (cfg.cert != null || cfg.key != null) 559 "+${pkgs.writers.writeBash "syncthing-copy-keys" '' 560 install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir} 561 ${optionalString (cfg.cert != null) '' 562 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.cert} ${cfg.configDir}/cert.pem 563 ''} 564 ${optionalString (cfg.key != null) '' 565 install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem 566 ''} 567 ''}" 568 ; 569 ExecStart = '' 570 ${cfg.package}/bin/syncthing \ 571 -no-browser \ 572 -gui-address=${cfg.guiAddress} \ 573 -home=${cfg.configDir} ${escapeShellArgs cfg.extraFlags} 574 ''; 575 MemoryDenyWriteExecute = true; 576 NoNewPrivileges = true; 577 PrivateDevices = true; 578 PrivateMounts = true; 579 PrivateTmp = true; 580 PrivateUsers = true; 581 ProtectControlGroups = true; 582 ProtectHostname = true; 583 ProtectKernelModules = true; 584 ProtectKernelTunables = true; 585 RestrictNamespaces = true; 586 RestrictRealtime = true; 587 RestrictSUIDSGID = true; 588 CapabilityBoundingSet = [ 589 "~CAP_SYS_PTRACE" "~CAP_SYS_ADMIN" 590 "~CAP_SETGID" "~CAP_SETUID" "~CAP_SETPCAP" 591 "~CAP_SYS_TIME" "~CAP_KILL" 592 ]; 593 }; 594 }; 595 syncthing-init = mkIf ( 596 cfg.devices != {} || cfg.folders != {} || cfg.extraOptions != {} 597 ) { 598 description = "Syncthing configuration updater"; 599 requisite = [ "syncthing.service" ]; 600 after = [ "syncthing.service" ]; 601 wantedBy = [ "multi-user.target" ]; 602 603 serviceConfig = { 604 User = cfg.user; 605 RemainAfterExit = true; 606 RuntimeDirectory = "syncthing-init"; 607 Type = "oneshot"; 608 ExecStart = updateConfig; 609 }; 610 }; 611 612 syncthing-resume = { 613 wantedBy = [ "suspend.target" ]; 614 }; 615 }; 616 }; 617}