12 October 2021: Package created.
8 February 2022: Added rrsync.
9 February 2022: Improved rrsync logging.
13-14 February 2022: Simplified logging line.
22 February 2022: Abort rsync-server.pl if no SSH_ORIGINAL_COMMAND env variable.
3 December 2023: Added pledge/unveil for OpenBSD to rrsync and rsync-client.
30 December 2023: Call pledge correctly.
31 December 2023: Expand unveil for /usr/share/zoneinfo.
1 January 2024: More corrections on pledge use.
2 January 2024: Updated rrsync to be more similar to python version.
4 January 2024: Updated rrsync error messages, rsync-client perl file open style.
10 February 2024: rsync-client: Improve unveiling of setup/cleanup commands, fix bug.
10 July 2024: rsync-client: unveil /bin/sh when * in dirlist & do minimal dirlist validation.
30 July 2024: Updated rrsync for locking and -no-lock, additional checks.
1 September 2024: Added pledge/unveil to rsync-altroot. Fixed minor bug
	(pledge error reporting) in rsync-client.pl.
8 October 2024: Added new s after dot for rsync-client.pl for rsync 3.3.0,
	changed no-w to no-W in rrsync.
1 November 2024: Make rsync-altroot.pl abort on mount error.
15 January 2025: Add "true" command to rrsync for parity with rsync 3.4.0's
	python version.
28 August 2025: Updated files/dirs for rsync-altroot.pl.
15 September 2025: rsync-client.pl, rrsync: support Linux rsync path.
26 September 2025: add error handling on log open in rsync-client.pl, remove
	deprecated rsync tools.
9 December 2025: add -no-overwrite to rrsync.
10 January 2026: rsync-client.pl: minor changes, less use of shell;
	rsync-altroot.pl: minor change, remove unnecessary quotes.
11 January 2026: rsync-client.pl: allow multiple setup/cleanup commands.
8 April 2026: rsync-client.pl remove shell requirement, simplify logic, fix bug
	that prevented pledge/unveil for server side.
	rsync-altroot.pl remove /etc special casing.
	rrsync use "use warnings".
19 April 2026: rsync-client.pl validates config file ownership and permissions,
	ensures log file permissions are 0600 on creation.
29 April 2026: rsync-client.pl rejects globs when doas/sudo is used.
2 May 2026: rsync-client.pl cleanup variety of minor issues.
17 May 2026: rrsync: fix exploitable vuln in glob processing order,
	remove DNS resolution on host names in logging, remove 'dns'
	pledge.
	rsync-altroot: remove bareword filehandles.
	rsync-client: add comments on setup/cleanup unveiling, no code change.
20 May 2026: rrsync: use original args in logging, no extra spaces after host/IP.
30 May 2026: rrsync: add missing 'dirs' long option (after comparing to python version
	which fixed it in December 2025).
	rsync-client: cosmetic change, convert constant $RSYNC_IDENTITY to variable
	$rsync_identity.
18 June 2026: rrsync: add -absolute option to match its addition to python version.
2 July 2026: rsync-altroot: fix bug preventing root files from rsyncing, block rsync
	if /altroot not mounted, some cleanup.
	rrsync: fix -absolute, log all validation errors, properly return rsync error
	codes.
        rsync-client.pl: Fix undefined behavior issue with $unveil_path, use block
        mode grep in option match, report unveil errors, add dest-sudo glob config
	check, improve option split to avoid leading space case. Left unfixed: system
	return codes not checked.
8 August 2026: rsync-client.pl: Improve portability for macOS and Linux.
13 August 2026: rrsync: add rsync 3.5.0 portable hardening:
	--confine-root and --drop-D (gated on the RSYNC_PRE35 constant for
	pre-3.5 hosts), disable peer-selected --debug, pass the
	--files-from stdin sentinel and refuse a remote --files-from in
	-wo mode, disable backup-dir/partial-dir/delay-updates/log-file
	and short -b under -no-overwrite, and open the log file with
	O_NOFOLLOW. Documented why the Linux-only /proc-fd TOCTOU pin is
	not ported.
	rsync-client.pl: Inject --confine-root on server side.
22 September 2026: rrsync: single character change for /-removal consistency
	in check_arg subroutine (add +).
15 October 2026: rrsync:
	- Remove e, f and i from the accepted short no-arg options (the 2
	January 2024 entry says they were removed; they were not). rsync
	gives -e and -f an argument, so a standalone "-e" swallowed the
	"." separator: the server rsync then chdir'd into the first
	transfer arg while rrsync had validated the rest relative to the
	restricted dir, so a client that had pushed a symlink to an
	outside directory could read and write through it, even with
	--confine-root (demonstrated with rsync 3.2.7 and 3.5.1). The
	server's -e.iLsfx... protocol string is still accepted by the
	(e\d*\.\w*)? group. Same set as the python version.
	- Escape control characters in everything written to the log, so a
	newline in SSH_ORIGINAL_COMMAND cannot forge log lines.
	- Lock: wait at most LOCK_WAIT seconds, then refuse and log,
	instead of blocking forever behind a hung holder (python refuses
	at once).
	- Exit with 127 if rsync could not be run and 128+signal if it was
	killed
	(a killed rsync used to exit 0).
	- Prefer Homebrew rsync on macOS when present (as rsync-client.pl
	does); /usr/bin/rsync there is openrsync or 2.6.9, which lack
	--confine-root.
	- Collapse // before deciding whether an arg is -absolute (python
	order).
	- Take _rsyncu's home directory (log and lock files) from the
	password database instead of assuming /home/_rsyncu (macOS homes
	are in /Users, where every run without -no-lock died opening the
	lock file).
	- Correct the OpenBSD comment: pledge/unveil confine rrsync
	only. The 17 May 2026 entry's "already mitigated on OpenBSD" was
	wrong for the same reason (an exec'd rsync gets no unveil); that
	claim came from a Claude analysis in the session that prompted it.

	rsync-client.pl:
	- Pass the ssh command to rsync as -e in argv instead of
	RSYNC_RSH. With RSYNC_RSH passed through doas (setenv) or sudo
	(env_keep), _rsyncu could run a whitelisted rsync command with its
	own RSYNC_RSH and so run any program as root, with a doas log line
	identical to a normal one
	(demonstrated with sudo and opendoas). The -e string now also
	gives ssh -o UserKnownHostsFile=<_rsyncu's known_hosts>, since an
	elevated ssh otherwise checks root's. Client-side elevated command
	lines change; see Security note 3. Remove setenv { RSYNC_RSH } /
	env_keep RSYNC_RSH.
	- Prefer doas when present, fall back to sudo (was: sudo whenever
	present).  Unveil the one in use only when a path in this pass
	needs it, and die with a message if it is needed and neither
	exists.
	- Escape control characters in client-supplied text written to the
	log, so a newline in SSH_ORIGINAL_COMMAND cannot forge log lines.
	- Config lists accept "," with or without following whitespace;
	lines holding only whitespace are treated as blank.
	- In "both" mode on OpenBSD, unveil a configured ssh-identity
	before the second pass tests it (the first pass's unveils are
	already in force).
        - Check exit statuses. Client: a failed setup skips that item's rsync
	(its cleanup still runs), a failed rsync or cleanup is reported,
	the remaining items still run, and the exit status is 1 if
	anything failed. rsync exit 24 (source files vanished during the
	transfer) is reported but not counted. A ";" chain stops at its
	first failure.  Server: a failed setup skips the rsync; a failed
	rsync is logged as "Command failed (<reason>): ..." instead of
	"Command executed: ..."; the exit status is rsync's, or 1 for a
	disallowed command, path or options or a failed setup or cleanup
	(it was always 0).
	- Accept -vlogDtprze.LsfxCIvu, the server option string a client
	sends for a pull with --delete-after or --delete-before (they turn
	off incremental recursion, so no "i"); such pulls were refused as
	a disallowed option (shown with rsync 3.2.7 and 3.5.1).
	- Documentation: the config list format, the doas/sudo command
	forms (including --confine-root on the server side), the key
	types. The 2026-08-13 entry's "if the config doesn't already
	specify it" has no code behind it and needs none: rsync does not
	forward --confine-root from client to server, so a --confine-root
	in rsync-options only confines the client's own rsync.

	rsync-altroot.pl:
	- Rsync the root directories only when fstab has an /altroot entry
	(and it is mounted). The 2026-07-01 mount check covered only the
	filesystems with an fstab entry, so with no /altroot line the root
	dirs were rsynced with --delete into /altroot on the root
	disk. (That check came from a Claude review; this was its gap.)
	- The mount check skips a filesystem whose source dir does not
	exist, as the mount and rsync loops already do (it used to abort
	the run).
	- Check the exit status of every rsync and umount; report failures
	and exit 1 at the end. rsync exit 24 (source files vanished during
	the transfer, normal on a live /var) is reported but is not a
	failure.
	- If a mount fails, unmount what this run mounted before dying, so
	the next run does not fail on "already mounted".
	- Exit 1 on an invalid option (was 0).
	- pledge/unveil: drop wpath/cpath and the /altroot rwc unveil. The
	script itself only reads (rsync, mount and umount are exec'd and
	are not confined by its unveil); unveil / r covers its reads.
