1 From 41d06042cb9c2d4070a732745305b85b6328b58f Mon Sep 17 00:00:00 2001
 
   2 From: Julien Moutinho <julm+nixpkgs@sourcephile.fr>
 
   3 Date: Sat, 27 Nov 2021 02:50:39 +0100
 
   4 Subject: [PATCH 2/2] nixos/syncoid: use DynamicUser=
 
   6 HistoryNote: the following changes
 
   7 were previously in separate commits
 
   8 but have been squashed and nixfmt-rfc-style
 
   9 on be able to rebase them
 
  10 onto a treewide change applying nixfmt-rfc-style
 
  11 on nixos/modules/services/backup/syncoid.nix
 
  12 which introduced multiple untrackable conflicts.
 
  16 - nixos/syncoid: add destroy to localTargetAllow's default
 
  18   syncoid[3282027]: Resuming interrupted zfs send/receive from rpool/var/mail to losurdo/backup/mermet/var/mail (~ UNKNOWN remaining):
 
  19   syncoid[3282885]: cannot resume send: 'rpool/var/mail@autosnap_2021-10-17_15:00:19_hourly' used in the initial send no longer exists
 
  20   syncoid[3282897]: cannot receive: failed to read from stream
 
  21   syncoid[3282027]: WARN: resetting partially receive state because the snapshot source no longer exists
 
  22   syncoid[3283326]: cannot destroy 'losurdo/backup/mermet/var/mail/%recv': permission denied
 
  24 - nixos/syncoid: allow @timer syscalls
 
  26 - nixos/syncoid: set TZ envvar
 
  28 - nixos/syncoid: test sending to multiple targets
 
  30 - nixos/syncoid: don't delegate permissions to source's parent
 
  32   See https://github.com/NixOS/nixpkgs/pull/165204
 
  33   and https://github.com/NixOS/nixpkgs/pull/180111
 
  35 - nixos/syncoid: add support for LoadCredentialEncrypted=
 
  37 - nixos/syncoid: zfs-unallow unused dynamic users
 
  39 - nixos/syncoid: use NFTSet=
 
  41 Changes between the treewide nixfmt-rfc-style
 
  42 and those changes have been preserved:
 
  44 - 71967c47e54c56557c0ee7bec7fe554c018e37c4 nixos/syncoid: allow interval to be list of strings
 
  45 - f34483be5ee2418a563545a56743b7b59c549935 nixosTests: handleTest -> runTest, batch 1
 
  47  nixos/modules/services/backup/syncoid.nix | 444 +++++++++++-----------
 
  48  nixos/tests/sanoid.nix                    |  78 ++--
 
  49  2 files changed, 265 insertions(+), 257 deletions(-)
 
  51 diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
 
  52 index 8b4c59155f..a011b9b24f 100644
 
  53 --- a/nixos/modules/services/backup/syncoid.nix
 
  54 +++ b/nixos/modules/services/backup/syncoid.nix
 
  57    cfg = config.services.syncoid;
 
  59 -  # Extract local dasaset names (so no datasets containing "@")
 
  60 +  # Extract local dataset names (so no datasets containing "@")
 
  63      lib.optionals (d != null) (
 
  64 @@ -20,81 +20,9 @@ let
 
  65    # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
 
  68 -    lib.concatMapStrings (s: if lib.isList s then "-" else s) (
 
  69 +    lib.concatMapStrings (s: if builtins.isList s then "-" else s) (
 
  70        builtins.split "[^a-zA-Z0-9_.\\-]+" name
 
  73 -  # Function to build "zfs allow" commands for the filesystems we've delegated
 
  74 -  # permissions to. It also checks if the target dataset exists before
 
  75 -  # delegating permissions, if it doesn't exist we delegate it to the parent
 
  76 -  # dataset (if it exists). This should solve the case of provisoning new
 
  79 -    permissions: dataset:
 
  81 -      "-+${pkgs.writeShellScript "zfs-allow-${dataset}" ''
 
  82 -        # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
 
  84 -        # Run a ZFS list on the dataset to check if it exists
 
  86 -          lib.escapeShellArgs [
 
  87 -            "/run/booted-system/sw/bin/zfs"
 
  91 -        } 2> /dev/null; then
 
  92 -          ${lib.escapeShellArgs [
 
  93 -            "/run/booted-system/sw/bin/zfs"
 
  96 -            (lib.concatStringsSep "," permissions)
 
  99 -        ${lib.optionalString ((builtins.dirOf dataset) != ".") ''
 
 101 -            ${lib.escapeShellArgs [
 
 102 -              "/run/booted-system/sw/bin/zfs"
 
 105 -              (lib.concatStringsSep "," permissions)
 
 106 -              # Remove the last part of the path
 
 107 -              (builtins.dirOf dataset)
 
 114 -  # Function to build "zfs unallow" commands for the filesystems we've
 
 115 -  # delegated permissions to. Here we unallow both the target but also
 
 116 -  # on the parent dataset because at this stage we have no way of
 
 117 -  # knowing if the allow command did execute on the parent dataset or
 
 118 -  # not in the pre-hook. We can't run the same if in the post hook
 
 119 -  # since the dataset should have been created at this point.
 
 120 -  buildUnallowCommand =
 
 121 -    permissions: dataset:
 
 123 -      "-+${pkgs.writeShellScript "zfs-unallow-${dataset}" ''
 
 124 -        # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
 
 125 -        ${lib.escapeShellArgs [
 
 126 -          "/run/booted-system/sw/bin/zfs"
 
 129 -          (lib.concatStringsSep "," permissions)
 
 132 -        ${lib.optionalString ((builtins.dirOf dataset) != ".") (
 
 133 -          lib.escapeShellArgs [
 
 134 -            "/run/booted-system/sw/bin/zfs"
 
 137 -            (lib.concatStringsSep "," permissions)
 
 138 -            # Remove the last part of the path
 
 139 -            (builtins.dirOf dataset)
 
 147 @@ -120,38 +48,23 @@ in
 
 151 -    user = lib.mkOption {
 
 152 -      type = lib.types.str;
 
 153 -      default = "syncoid";
 
 154 -      example = "backup";
 
 156 -        The user for the service. ZFS privilege delegation will be
 
 157 -        automatically configured for any local pools used by syncoid if this
 
 158 -        option is set to a user other than root. The user will be given the
 
 159 -        "hold" and "send" privileges on any pool that has datasets being sent
 
 160 -        and the "create", "mount", "receive", and "rollback" privileges on
 
 161 -        any pool that has datasets being received.
 
 165 -    group = lib.mkOption {
 
 166 -      type = lib.types.str;
 
 167 -      default = "syncoid";
 
 168 -      example = "backup";
 
 169 -      description = "The group for the service.";
 
 172      sshKey = lib.mkOption {
 
 173        type = with lib.types; nullOr (coercedTo path toString str);
 
 176 -        SSH private key file to use to login to the remote system. Can be
 
 177 -        overridden in individual commands.
 
 178 +        SSH private key file to use to login to the remote system.
 
 179 +        It can be overridden in individual commands.
 
 180 +        It is loaded using `LoadCredentialEncrypted=`
 
 181 +        when its path is prefixed by a credential name and colon,
 
 182 +        otherwise `LoadCredential=` is used.
 
 183 +        For more SSH tuning, you may use syncoid's `--sshoption`
 
 184 +        in {option}`services.syncoid.commonArgs`
 
 185 +        and/or in the `extraArgs` of a specific command.
 
 189      localSourceAllow = lib.mkOption {
 
 190 -      type = lib.types.listOf lib.types.str;
 
 191 +      type = with lib.types; listOf str;
 
 192        # Permissions snapshot and destroy are in case --no-sync-snap is not used
 
 195 @@ -170,11 +83,15 @@ in
 
 198      localTargetAllow = lib.mkOption {
 
 199 -      type = lib.types.listOf lib.types.str;
 
 200 +      type = with lib.types; listOf str;
 
 201 +      # Permission destroy is required to reset broken receive states (zfs receive -A),
 
 202 +      # which syncoid does when it fails to resume a receive state,
 
 203 +      # when the snapshot it refers to has been destroyed on the source.
 
 212 @@ -198,7 +115,7 @@ in
 
 215      commonArgs = lib.mkOption {
 
 216 -      type = lib.types.listOf lib.types.str;
 
 217 +      type = with lib.types; listOf str;
 
 219        example = [ "--no-sync-snap" ];
 
 221 @@ -296,13 +213,13 @@ in
 
 225 -              useCommonArgs = lib.mkOption {
 
 226 -                type = lib.types.bool;
 
 229 -                  Whether to add the configured common arguments to this command.
 
 233 +                lib.mkEnableOption ''
 
 234 +                  configured common arguments to this command
 
 240                service = lib.mkOption {
 
 241                  type = lib.types.attrs;
 
 242 @@ -341,139 +258,216 @@ in
 
 245    config = lib.mkIf cfg.enable {
 
 247 -      users = lib.mkIf (cfg.user == "syncoid") {
 
 250 -          isSystemUser = true;
 
 251 -          # For syncoid to be able to create /var/lib/syncoid/.ssh/
 
 252 -          # and to use custom ssh_config or known_hosts.
 
 253 -          home = "/var/lib/syncoid";
 
 254 -          createHome = false;
 
 257 -      groups = lib.mkIf (cfg.group == "syncoid") {
 
 262      systemd.services = lib.mapAttrs' (
 
 265 +        sshKeyCred = builtins.split ":" c.sshKey;
 
 267        lib.nameValuePair "syncoid-${escapeUnitName name}" (
 
 270              description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
 
 271              after = [ "zfs.target" ];
 
 272              startAt = cfg.interval;
 
 273 -            # syncoid may need zpool to get feature@extensible_dataset
 
 274 -            path = [ "/run/booted-system/sw/bin/" ];
 
 277 -                (map (buildAllowCommand c.localSourceAllow) (localDatasetName c.source))
 
 278 -                ++ (map (buildAllowCommand c.localTargetAllow) (localDatasetName c.target));
 
 280 -                (map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source))
 
 281 -                ++ (map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
 
 282 -              ExecStart = lib.escapeShellArgs (
 
 283 -                [ "${cfg.package}/bin/syncoid" ]
 
 284 -                ++ lib.optionals c.useCommonArgs cfg.commonArgs
 
 285 -                ++ lib.optional c.recursive "-r"
 
 286 -                ++ lib.optionals (c.sshKey != null) [
 
 296 -                  "--no-privilege-elevation"
 
 303 -              StateDirectory = [ "syncoid" ];
 
 304 -              StateDirectoryMode = "700";
 
 305 -              # Prevent SSH control sockets of different syncoid services from interfering
 
 307 -              # Permissive access to /proc because syncoid
 
 308 -              # calls ps(1) to detect ongoing `zfs receive`.
 
 309 -              ProcSubset = "all";
 
 310 -              ProtectProc = "default";
 
 311 +            # Here we explicitly use the booted system to guarantee the stable API needed by ZFS.
 
 312 +            # Moreover syncoid may need zpool to get feature@extensible_dataset.
 
 313 +            path = [ "/run/booted-system/sw" ];
 
 314 +            # Prevents missing snapshots during DST changes
 
 315 +            environment.TZ = "UTC";
 
 316 +            # A custom LD_LIBRARY_PATH is needed to access in `getent passwd`
 
 317 +            # the systemd's entry about the DynamicUser=,
 
 318 +            # so that ssh won't fail with: "No user exists for uid $UID".
 
 319 +            environment.LD_LIBRARY_PATH = config.system.nssModules.path;
 
 323 +                  # Recursively remove any residual permissions
 
 324 +                  # given on local+descendant datasets (source, target or target's parent)
 
 325 +                  # to any currently unknown (hence unused) systemd dynamic users (UID/GID range 61184…65519),
 
 326 +                  # which happens when a crash has occurred
 
 327 +                  # during any previous run of a syncoid-*.service (not only this one).
 
 332 +                      + pkgs.writeShellScript "zfs-unallow-unused-dynamic-users" ''
 
 335 +                        sed -ne 's/^\t\(user\|group\) (unknown: \([0-9]\+\)).*/\1 \2/p' |
 
 338 +                          while read -r role id; do
 
 339 +                            if [ "$id" -ge 61184 ] && [ "$id" -le 65519 ]; then
 
 341 +                                (user) uids+=("$id");;
 
 345 +                          zfs unallow -r -u "$(printf %s, "''${uids[@]}")" "$1"
 
 349 +                      + lib.escapeShellArg dataset
 
 352 +                      localDatasetName c.source
 
 353 +                      ++ localDatasetName c.target
 
 354 +                      ++ map builtins.dirOf (localDatasetName c.target)
 
 357 +                    # For a local source, allow the localSourceAllow ZFS permissions.
 
 360 +                      "+/run/booted-system/sw/bin/zfs allow $USER "
 
 361 +                      + lib.escapeShellArgs [
 
 362 +                        (lib.concatStringsSep "," c.localSourceAllow)
 
 365 +                    ) (localDatasetName c.source)
 
 367 +                    # For a local target, check if the dataset exists before delegating permissions,
 
 368 +                    # and if it doesn't exist, delegate it to the parent dataset.
 
 369 +                    # This should solve the case of provisioning new datasets.
 
 373 +                      + pkgs.writeShellScript "zfs-allow-target" ''
 
 375 +                        # Run a ZFS list on the dataset to check if it exists
 
 376 +                        zfs list "$dataset" >/dev/null 2>/dev/null ||
 
 377 +                          dataset="$(dirname "$dataset")"
 
 378 +                        zfs allow "$USER" ${lib.escapeShellArg (lib.concatStringsSep "," c.localTargetAllow)} "$dataset"
 
 381 +                      + lib.escapeShellArg dataset
 
 382 +                    ) (localDatasetName c.target);
 
 385 +                    zfsUnallow = dataset: "+/run/booted-system/sw/bin/zfs unallow $USER " + lib.escapeShellArg dataset;
 
 387 +                  map zfsUnallow (localDatasetName c.source)
 
 389 +                    # For a local target, unallow both the dataset and its parent,
 
 390 +                    # because at this stage we have no way of knowing if the allow command
 
 391 +                    # did execute on the parent dataset or not in the ExecStartPre=.
 
 392 +                    # We can't run the same if-then-else in the post hook
 
 393 +                    # since the dataset should have been created at this point.
 
 394 +                    lib.concatMap (dataset: [
 
 395 +                      (zfsUnallow dataset)
 
 396 +                      (zfsUnallow (builtins.dirOf dataset))
 
 397 +                    ]) (localDatasetName c.target);
 
 398 +                ExecStart = lib.escapeShellArgs (
 
 399 +                  [ "${cfg.package}/bin/syncoid" ]
 
 400 +                  ++ lib.optionals c.useCommonArgs cfg.commonArgs
 
 401 +                  ++ lib.optional c.recursive "--recursive"
 
 402 +                  ++ lib.optionals (c.sshKey != null) [
 
 404 +                    "\${CREDENTIALS_DIRECTORY}/${if lib.length sshKeyCred > 1 then lib.head sshKeyCred else "sshKey"}"
 
 412 +                    "--no-privilege-elevation"
 
 417 +                DynamicUser = true;
 
 418 +                NFTSet = lib.optional config.networking.nftables.enable "user:inet:filter:nixos_syncoid_uids";
 
 419 +                # Prevent SSH control sockets of different syncoid services from interfering
 
 421 +                # Permissive access to /proc because syncoid
 
 422 +                # calls ps(1) to detect ongoing `zfs receive`.
 
 423 +                ProcSubset = "all";
 
 424 +                ProtectProc = "default";
 
 426 -              # The following options are only for optimizing:
 
 427 -              # systemd-analyze security | grep syncoid-'*'
 
 428 -              AmbientCapabilities = "";
 
 429 -              CapabilityBoundingSet = "";
 
 430 -              DeviceAllow = [ "/dev/zfs" ];
 
 431 -              LockPersonality = true;
 
 432 -              MemoryDenyWriteExecute = true;
 
 433 -              NoNewPrivileges = true;
 
 434 -              PrivateDevices = true;
 
 435 -              PrivateMounts = true;
 
 436 -              PrivateNetwork = lib.mkDefault false;
 
 437 -              PrivateUsers = false; # Enabling this breaks on zfs-2.2.0
 
 438 -              ProtectClock = true;
 
 439 -              ProtectControlGroups = true;
 
 440 -              ProtectHome = true;
 
 441 -              ProtectHostname = true;
 
 442 -              ProtectKernelLogs = true;
 
 443 -              ProtectKernelModules = true;
 
 444 -              ProtectKernelTunables = true;
 
 445 -              ProtectSystem = "strict";
 
 447 -              RestrictAddressFamilies = [
 
 452 -              RestrictNamespaces = true;
 
 453 -              RestrictRealtime = true;
 
 454 -              RestrictSUIDSGID = true;
 
 455 -              RootDirectory = "/run/syncoid/${escapeUnitName name}";
 
 456 -              RootDirectoryStartOnly = true;
 
 457 -              BindPaths = [ "/dev/zfs" ];
 
 458 -              BindReadOnlyPaths = [
 
 464 -              # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
 
 465 -              InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
 
 466 -              MountAPIVFS = true;
 
 467 -              # Create RootDirectory= in the host's mount namespace.
 
 468 -              RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
 
 469 -              RuntimeDirectoryMode = "700";
 
 470 -              SystemCallFilter = [
 
 472 -                # Groups in @system-service which do not contain a syscall listed by:
 
 473 -                # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
 
 474 -                # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
 
 475 -                # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
 
 485 -              SystemCallArchitectures = "native";
 
 486 -              # This is for BindPaths= and BindReadOnlyPaths=
 
 487 -              # to allow traversal of directories they create in RootDirectory=.
 
 490 +                # The following options are only for optimizing:
 
 491 +                # systemd-analyze security | grep syncoid-'*'
 
 492 +                AmbientCapabilities = "";
 
 493 +                CapabilityBoundingSet = "";
 
 494 +                DeviceAllow = [ "/dev/zfs" ];
 
 495 +                LockPersonality = true;
 
 496 +                MemoryDenyWriteExecute = true;
 
 497 +                NoNewPrivileges = true;
 
 498 +                PrivateDevices = true;
 
 499 +                PrivateMounts = true;
 
 500 +                PrivateNetwork = lib.mkDefault false;
 
 501 +                PrivateUsers = false; # Enabling this breaks on zfs-2.2.0
 
 502 +                ProtectClock = true;
 
 503 +                ProtectControlGroups = true;
 
 504 +                ProtectHome = true;
 
 505 +                ProtectHostname = true;
 
 506 +                ProtectKernelLogs = true;
 
 507 +                ProtectKernelModules = true;
 
 508 +                ProtectKernelTunables = true;
 
 509 +                ProtectSystem = "strict";
 
 511 +                RestrictAddressFamilies = [
 
 516 +                RestrictNamespaces = true;
 
 517 +                RestrictRealtime = true;
 
 518 +                RestrictSUIDSGID = true;
 
 519 +                RootDirectory = "/run/syncoid/${escapeUnitName name}";
 
 520 +                BindPaths = [ "/dev/zfs" ];
 
 521 +                BindReadOnlyPaths = [
 
 525 +                  # Some programs hardcode /var/run
 
 526 +                  # eg. /var/run/avahi-daemon/socket to resolve *.local mDNS domains
 
 530 +                # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
 
 531 +                InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
 
 532 +                MountAPIVFS = true;
 
 533 +                # Create RootDirectory= in the host's mount namespace.
 
 534 +                RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
 
 535 +                RuntimeDirectoryMode = "700";
 
 536 +                SystemCallFilter = [
 
 538 +                  # Groups in @system-service which do not contain a syscall listed by:
 
 539 +                  # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
 
 540 +                  # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
 
 541 +                  # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
 
 550 +                SystemCallArchitectures = "native";
 
 551 +                # This is for BindPaths= and BindReadOnlyPaths=
 
 552 +                # to allow traversal of directories they create in RootDirectory=.
 
 556 +                if lib.length sshKeyCred > 1 then
 
 557 +                  { LoadCredentialEncrypted = [ c.sshKey ]; }
 
 559 +                  { LoadCredential = [ "sshKey:${c.sshKey}" ]; }
 
 568 +    networking.nftables.ruleset = lib.mkBefore ''
 
 569 +      table inet filter {
 
 570 +        # A set containing the dynamic UIDs of the syncoid services currently active
 
 571 +        set nixos_syncoid_uids { typeof meta skuid; }
 
 576    meta.maintainers = with lib.maintainers; [
 
 577 diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix
 
 578 index e42fd54dfd..eabe88b6be 100644
 
 579 --- a/nixos/tests/sanoid.nix
 
 580 +++ b/nixos/tests/sanoid.nix
 
 581 @@ -50,16 +50,26 @@ in
 
 583            sshKey = "/var/lib/syncoid/id_ecdsa";
 
 585 -            # Sync snapshot taken by sanoid
 
 587 -              target = "root@target:pool/sanoid";
 
 588 +            # Take snapshot and sync
 
 589 +            "pool/syncoid".target = "root@target:pool/syncoid";
 
 591 +            # Sync the same dataset to different targets
 
 593 +              source = "pool/sanoid";
 
 594 +              target = "root@target:pool/sanoid1";
 
 597 +                "--create-bookmark"
 
 601 +              source = "pool/sanoid";
 
 602 +              target = "root@target:pool/sanoid2";
 
 608 -            # Take snapshot and sync
 
 609 -            "pool/syncoid".target = "root@target:pool/syncoid";
 
 611              # Test pool without parent (regression test for https://github.com/NixOS/nixpkgs/pull/180111)
 
 612              "pool".target = "root@target:pool/full-pool";
 
 613 @@ -94,6 +104,7 @@ in
 
 614          "zfs create pool/syncoid",
 
 620          "parted --script /dev/vdb -- mklabel msdos mkpart primary 1024M -1s",
 
 621 @@ -106,41 +117,44 @@ in
 
 622          "mkdir -m 700 -p /var/lib/syncoid",
 
 623          "cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa",
 
 624          "chmod 600 /var/lib/syncoid/id_ecdsa",
 
 625 -        "chown -R syncoid:syncoid /var/lib/syncoid/",
 
 628 -    assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting"
 
 629 -    assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting"
 
 630 -    assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting"
 
 631 +    with subtest("Take snapshots with sanoid"):
 
 632 +      source.succeed("touch /mnt/pool/sanoid/test.txt")
 
 633 +      source.succeed("touch /mnt/pool/compat/test.txt")
 
 634 +      source.systemctl("start --wait sanoid.service")
 
 636 -    # Take snapshot with sanoid
 
 637 -    source.succeed("touch /mnt/pool/sanoid/test.txt")
 
 638 -    source.succeed("touch /mnt/pool/compat/test.txt")
 
 639 -    source.systemctl("start --wait sanoid.service")
 
 640 +    # Add some unused dynamic users to the stateful allow list of ZFS datasets,
 
 641 +    # simulating a state where they remain after the system crashed,
 
 642 +    # to check they'll be correctly removed by the syncoid services.
 
 643 +    # Each syncoid service run from now may reuse at most one of them for itself.
 
 645 +        "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool",
 
 646 +        "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/sanoid",
 
 647 +        "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/syncoid",
 
 650 +    with subtest("sync snapshots"):
 
 651 +      target.wait_for_open_port(22)
 
 652 +      source.succeed("touch /mnt/pool/syncoid/test.txt")
 
 654 +      source.systemctl("start --wait syncoid-pool-syncoid.service")
 
 655 +      target.succeed("cat /mnt/pool/syncoid/test.txt")
 
 657 +      source.systemctl("start --wait syncoid-pool-sanoid{1,2}.service")
 
 658 +      target.succeed("cat /mnt/pool/sanoid1/test.txt")
 
 659 +      target.succeed("cat /mnt/pool/sanoid2/test.txt")
 
 661 +      source.systemctl("start --wait syncoid-pool.service")
 
 662 +      target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]")
 
 664 +      source.systemctl("start --wait syncoid-pool-compat.service")
 
 665 +      target.succeed("cat /mnt/pool/compat/test.txt")
 
 667      assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting"
 
 668      assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting"
 
 669      assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting"
 
 672 -    target.wait_for_open_port(22)
 
 673 -    source.succeed("touch /mnt/pool/syncoid/test.txt")
 
 674 -    source.systemctl("start --wait syncoid-pool-sanoid.service")
 
 675 -    target.succeed("cat /mnt/pool/sanoid/test.txt")
 
 676 -    source.systemctl("start --wait syncoid-pool-syncoid.service")
 
 677 -    source.systemctl("start --wait syncoid-pool-syncoid.service")
 
 678 -    target.succeed("cat /mnt/pool/syncoid/test.txt")
 
 680      assert(len(source.succeed("zfs list -H -t snapshot pool/syncoid").splitlines()) == 1), "Syncoid should only retain one sync snapshot"
 
 682 -    source.systemctl("start --wait syncoid-pool.service")
 
 683 -    target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]")
 
 685 -    source.systemctl("start --wait syncoid-pool-compat.service")
 
 686 -    target.succeed("cat /mnt/pool/compat/test.txt")
 
 688 -    assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots"
 
 689 -    assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots"
 
 690 -    assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots"