# Conflicts: # RSYNC_COMPAT.md # src/shared/config.c # src/shared/config.h # src/shared/identity.c # tests/integration/test_preflight.py # tests/test_client_cli.c # tests/test_config.c
707 lines
36 KiB
C
707 lines
36 KiB
C
#ifndef CONFIG_H
|
|
#define CONFIG_H
|
|
|
|
#include "array_list.h"
|
|
#include "checksum.h"
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
|
|
|
|
/* --outbuf stdout/stderr buffering style (client-only launch concern, never
|
|
* crosses the wire). OUTBUF_BLOCK is the default, matching the stdio default
|
|
* (fully buffered when output is not a terminal). */
|
|
typedef enum {
|
|
OUTBUF_BLOCK = 0, /* _IOFBF */
|
|
OUTBUF_LINE, /* _IOLBF */
|
|
OUTBUF_NONE /* _IONBF */
|
|
} OutbufMode;
|
|
|
|
/* Receiver-side staging state for --delay-updates. Forward-declared here so
|
|
Config can carry it; the concrete type lives in delay_updates.h. */
|
|
typedef struct DelayUpdatesContext DelayUpdatesContext;
|
|
|
|
/* Alternate basis-directory modes (--compare-dest / --copy-dest /
|
|
* --link-dest). Each flag adds one entry to the ordered Config->basis_dirs
|
|
* list; the receiver consults entries in command-line order and stops at the
|
|
* first exact match, mirroring rsync's basis-dir priority rules. */
|
|
typedef enum {
|
|
BASIS_DEST_NONE = 0,
|
|
BASIS_DEST_COMPARE, /* compare only: never copies, never materializes */
|
|
BASIS_DEST_COPY, /* local copy of the matched basis file */
|
|
BASIS_DEST_LINK /* hard link to the matched basis file */
|
|
} BasisDestType;
|
|
|
|
typedef struct BasisDest {
|
|
BasisDestType type;
|
|
char* path; /* relative to the destination root (receiver-confined) */
|
|
} BasisDest;
|
|
|
|
/* One resolved FROM:TO identity-mapping rule (--usermap / --groupmap). Both
|
|
* fields are numeric ids. IDENTITY_MATCH_ANY (-1) in `from` is rsync's '*'
|
|
* wildcard (matches any transmitted id); IDENTITY_CURRENT (-1) in `to` makes
|
|
* the receiver resolve the receiving process's own current euid/egid at apply
|
|
* time. Names are resolved to numbers at parse time on the client (see
|
|
* identity.h for the exact subset). */
|
|
typedef struct {
|
|
int32_t from;
|
|
int32_t to;
|
|
} IdentityMap;
|
|
|
|
/* --sockopts=OPTIONS allowlist. Only these option names are accepted; anything
|
|
* else is rejected (never silently ignored). TCP_NODELAY, SO_KEEPALIVE and
|
|
* SO_REUSEADDR are boolean options (value 0/1); SO_RCVBUF and SO_SNDBUF take a
|
|
* non-negative byte count. All are applied as int-sized setsockopt values. */
|
|
typedef enum {
|
|
SOCKOPT_TCP_NODELAY = 0,
|
|
SOCKOPT_SO_KEEPALIVE,
|
|
SOCKOPT_SO_RCVBUF,
|
|
SOCKOPT_SO_SNDBUF,
|
|
SOCKOPT_SO_REUSEADDR,
|
|
SOCKOPT_COUNT
|
|
} SockOptId;
|
|
|
|
typedef struct {
|
|
SockOptId id; /* allowlist index */
|
|
int value; /* 0/1 for booleans, byte count for SO_RCVBUF/SO_SNDBUF */
|
|
} SockOptEntry;
|
|
|
|
typedef struct Config {
|
|
char* version;
|
|
char* send_directory;
|
|
char* receive_root_directory;
|
|
bool save_to_disk;
|
|
bool use_multithreading;
|
|
bool use_chunk_serialization;
|
|
bool use_compression;
|
|
bool use_sendfile;
|
|
bool use_metadata;
|
|
bool use_executability;
|
|
bool metadata_explicitly_disabled;
|
|
bool show_progress;
|
|
bool dry_run;
|
|
bool remove_source_files;
|
|
bool use_delete;
|
|
int compression_level;
|
|
int compression_threads;
|
|
unsigned long long chunk_size;
|
|
int ssh_port;
|
|
TransportType transport;
|
|
char* ssh_destination;
|
|
/* Daemon module selection (Wave A, protocol 2.15.0). Client-composed from a
|
|
* host::module/path destination; NULL or "" means "no module" (the ordinary
|
|
* standalone-server path). Crosses the wire as a trailing config-frame
|
|
* string so the daemon can look the module up in its own config and confine
|
|
* the connection to the module's root (never a client-chosen root). */
|
|
char* module;
|
|
/* Daemon password authentication (Wave B, protocol 2.15.0, WITHIN the Wave A
|
|
* frame layout -- see the PROTOCOL_VERSION note below for why this is not a
|
|
* bump). Client-composed from a --password-file whose first meaningful line
|
|
* is `user:password`: the client sends ONLY the username and a SHA-256 hex
|
|
* digest of the password (auth_user + auth_password_hash), never the literal
|
|
* password. Both are NULL when the client has no credentials to present; a
|
|
* module WITHOUT `auth users` stays open and the server ignores any
|
|
* credentials that do arrive (the client sends them opportunistically and
|
|
* the server decides). */
|
|
char* auth_user;
|
|
char* auth_password_hash;
|
|
/* Client-only path of --password-file (never crosses the wire; it is read to
|
|
* populate auth_user/auth_password_hash before connecting). */
|
|
char* password_file;
|
|
char* fastsync_server_path;
|
|
/* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the
|
|
* charset of FILE NAMES at the wire boundary. CONVERT_SPEC is
|
|
* "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is
|
|
* the remote side's charset and defaults to LOCAL. The sender converts
|
|
* every path LOCAL->REMOTE before transmitting it; the receiver converts
|
|
* every received path back REMOTE->LOCAL before creating/writing it. The
|
|
* FULL SPEC crosses the wire as a trailing config-frame string so each end
|
|
* derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL
|
|
* (or "") means no conversion: identity with zero overhead. See charset.c
|
|
* and the PROTOCOL_VERSION note below. */
|
|
char* iconv_spec;
|
|
char** exclude_patterns;
|
|
int exclude_count;
|
|
char** include_patterns;
|
|
int include_count;
|
|
unsigned long long max_size;
|
|
unsigned long long min_size;
|
|
unsigned long long max_alloc;
|
|
bool use_incremental;
|
|
bool ignore_times;
|
|
bool size_only;
|
|
bool use_delta;
|
|
bool whole_file;
|
|
/* -y/--fuzzy: when a file must be transferred and the destination holds no
|
|
* usable file at the exact path, the receiver may reuse a SIMILAR-named
|
|
* existing regular file in the same destination directory as the delta
|
|
* basis so the sender transmits only the differences. Crosses the wire
|
|
* (the receiver performs the candidate search); the CLI implies
|
|
* --incremental + --delta because the similar-basis only matters on the
|
|
* receiver-driven delta path. Off by default. */
|
|
bool fuzzy;
|
|
int modify_window;
|
|
uint32_t delta_block_size;
|
|
unsigned long long delta_max_file_size;
|
|
bool use_tls;
|
|
char* server_host;
|
|
int server_port;
|
|
char* tls_cert;
|
|
char* tls_key;
|
|
char* tls_ca;
|
|
int timeout;
|
|
int contimeout;
|
|
bool quiet;
|
|
bool backup;
|
|
char* backup_dir;
|
|
bool stats;
|
|
int max_depth;
|
|
FILE* log_file;
|
|
int queue_size;
|
|
bool follow_symlinks;
|
|
bool partial;
|
|
|
|
// Issue #120: Symlink handling
|
|
bool copy_links;
|
|
bool safe_links;
|
|
bool copy_unsafe_links;
|
|
/* Phase 4 symlink-trust. -k/--copy-dirlinks and --munge-links are
|
|
* CLIENT/sender-side only (they decide how the SENDER scans and rewrites
|
|
* symlinks; the receiver never reads them), so they never cross the wire.
|
|
* -K/--keep-dirlinks is a RECEIVER-side policy (follow an in-root destination
|
|
* symlink-to-directory as a directory) and CROSSES the wire along with
|
|
* --munge-links (so the receiver knows to unmunge). */
|
|
bool copy_dirlinks; /* client-only, sender-side (-k) */
|
|
bool munge_links; /* crosses the wire */
|
|
bool keep_dirlinks; /* crosses the wire (-K) */
|
|
|
|
// Issue #121: Extended metadata preservation
|
|
bool preserve_hard_links;
|
|
bool preserve_acls;
|
|
bool preserve_xattrs;
|
|
bool preserve_devices;
|
|
bool preserve_sparse;
|
|
/* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device
|
|
* nodes on the destination by recreating them (mknod/mkfifo) instead of
|
|
* transferring content. preserve_specials mirrors rsync --specials (the
|
|
* special-file half of -D); preserve_devices mirrors --devices (the device
|
|
* half of -D); both CROSS the wire so the receiver knows a special/device
|
|
* entry must be recreated rather than written as a regular file. */
|
|
bool preserve_specials;
|
|
/* --copy-devices: copy the CONTENT of a source device as an ordinary regular
|
|
* file on the destination (rsync's non-privileged safe mode), instead of
|
|
* recreating the device node. CROSSES the wire (receiver treats the entry as
|
|
* a regular file, which is the default, so this is belt-and-braces). */
|
|
bool copy_devices;
|
|
/* --write-devices: write the received data directly INTO an existing device
|
|
* node on the destination instead of creating a regular file. Dangeroud;
|
|
* see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */
|
|
bool write_devices;
|
|
|
|
// Issue #122: Output/logging options
|
|
bool itemize_changes;
|
|
char* out_format;
|
|
char* log_file_format;
|
|
int info_level;
|
|
int debug_level;
|
|
bool list_only;
|
|
bool human_readable;
|
|
bool eight_bit_output;
|
|
|
|
// Issue #127: Transfer modes
|
|
bool existing;
|
|
bool ignore_existing;
|
|
bool update;
|
|
bool inplace;
|
|
bool delay_updates;
|
|
bool use_fsync;
|
|
bool append;
|
|
bool append_verify;
|
|
/* --preallocate: allocates the destination file's full expected space up
|
|
* front (before any data is written) so a transfer that would overflow disk
|
|
* fails fast at allocation time and the file is laid out contiguously,
|
|
* avoiding fragmentation. Receiver-side, crosses the wire. */
|
|
bool preallocate;
|
|
|
|
// Issue #128: Extended delete options
|
|
/* --delete-excluded: also delete destination entries that were excluded on
|
|
* the source. Default (off) matches rsync: excluded paths are protected from
|
|
* deletion. Crosses the wire (the sender encodes the choice by whether it
|
|
* transmits a protected-prefix list with the keep-set manifest). */
|
|
bool delete_excluded;
|
|
bool delete_after;
|
|
/* --max-delete=NUM: the receiver refuses to delete more than NUM entries per
|
|
* run (all-or-nothing: when the extras would exceed NUM nothing is removed and
|
|
* the transfer fails with a distinct error). -1 == no client limit (the
|
|
* server hard bound MAX_SERVER_DELETE_COUNT still applies). */
|
|
int max_delete;
|
|
/* --ignore-errors (client-only, never serialized): a sender-side source I/O
|
|
* error (an unreadable directory during the scan) normally aborts the run so
|
|
* no deletion happens; with --ignore-errors the scan continues and the
|
|
* (partial) keep-set is still transmitted so the deletion runs. */
|
|
bool ignore_errors;
|
|
/* --force (receiver-side): a regular file may replace a destination
|
|
* directory by removing that (possibly non-empty, symlink-safe) directory
|
|
* tree first, instead of failing the write. Crosses the wire. */
|
|
bool force_delete;
|
|
/* --ignore-missing-args (client-only, never serialized): a --files-from
|
|
* entry that does not exist under the source is silently skipped instead of
|
|
* failing the run. Sender-side only: nothing is sent for it and it never
|
|
* enters the keep-set. Implied by --delete-missing-args. */
|
|
bool ignore_missing_args;
|
|
/* --delete-missing-args: implies --ignore-missing-args; additionally each
|
|
* missing entry's destination mirror (computed like a present entry's wire
|
|
* path) is deleted receiver-side. Crosses the wire and is gated by the
|
|
* server's --allow-delete policy like --delete. rsync-parity: independent
|
|
* of ordinary --delete processing (it does not imply --delete); a non-empty
|
|
* directory mirror is only removed with --force or --delete in effect, and
|
|
* the missing-args deletions are not counted toward --max-delete. */
|
|
bool delete_missing_args;
|
|
|
|
// Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are
|
|
// never serialized to the wire (the receiver must not learn them).
|
|
ArrayList* filters; /* --filter=RULE rule strings, in order */
|
|
char* files_from; /* --files-from path (may be NULL) */
|
|
void* files_from_set; /* parsed FileListSet* allow-set, or NULL */
|
|
bool from0; /* -0/--from0: NUL-delimited *-from files */
|
|
bool cvs_exclude; /* -C/--cvs-exclude: standard CVS ignore set */
|
|
bool per_dir_filter; /* -F: apply per-directory .rsync-filter files */
|
|
bool prune_empty_dirs;
|
|
bool one_file_system; /* -x/--one-file-system: do not cross filesystem boundaries */
|
|
/* -R/--relative: crosses the wire; with --files-from listed entries keep
|
|
* their bare relative destination path (no source-root mirror prefix). */
|
|
bool relative;
|
|
/* --no-implied-dirs: client-only. With -R + --files-from, refuse to place a
|
|
* listed file whose ancestor directory is not itself explicitly listed. */
|
|
bool no_implied_dirs;
|
|
/* -d/--dirs: client-only. Transfer the directory entries named by the
|
|
* source argument / --files-from list without recursing into contents. */
|
|
bool dirs;
|
|
/* --mkpath: crosses the wire. Tells the server to create the destination
|
|
* root directory (and missing leading components below its authorized root)
|
|
* at connection start instead of requiring it to already exist. */
|
|
bool mkpath;
|
|
|
|
// Issue #130: Remote shell/connection options
|
|
/* -e/--rsh: the remote-shell program used to establish the SSH transport.
|
|
* NULL means the default "ssh". Client-only launch concern: NEVER crosses
|
|
* the wire (it is not meaningful to the daemon/server handshake). */
|
|
char* rsh_command;
|
|
/* --blocking-io: leave the SSH transport socket without
|
|
* SO_RCVTIMEO/SO_SNDTIMEO so it blocks naturally instead of timing out.
|
|
* Client-only launch concern: NEVER crosses the wire. */
|
|
bool blocking_io;
|
|
/* --outbuf mode (OutbufMode): stdout/stderr buffering. Client-only launch
|
|
* concern: NEVER crosses the wire. */
|
|
int outbuf;
|
|
bool old_args;
|
|
char* temp_dir;
|
|
/* --remote-option=OPT (Phase 5, long form only): one or more extra command-line
|
|
* options to append to the REMOTE server invocation over SSH. CLIENT-ONLY:
|
|
* they are composed into the remote command line by ssh_build_remote_command()
|
|
* (each valid word is shell-escaped with the same quoting boundary as the
|
|
* server path), and are NEVER serialized into the binary config frame. They
|
|
* do NOT cross the wire and are never parsed on the receiver process. */
|
|
char** remote_options;
|
|
int remote_option_count;
|
|
/* Alternate basis directories, ordered by command-line appearance. Each
|
|
* entry's type selects compare/copy/link behavior on an exact match. These
|
|
* cross the wire so the receiver can consult them; they are interpreted
|
|
* relative to the destination root and confined there. */
|
|
BasisDest* basis_dirs;
|
|
int basis_count;
|
|
|
|
// PR #174: Partial transfer resumption
|
|
char* partial_dir;
|
|
|
|
// PR #178: Backup versioning
|
|
char* suffix;
|
|
|
|
// PR #179: Delete policies
|
|
bool delete_before;
|
|
|
|
/* rsync deletion-timing family (real from Phase 3). At most one of
|
|
delete_before / delete_during / delete_delay / delete_after may be set, and
|
|
only together with use_delete (the CLI implies --delete for each of them).
|
|
delete_before and delete_during select the EARLY engine mode: the keep-set
|
|
manifest is transmitted before any file data and extras are removed then,
|
|
acknowledged, before the first data byte. delete_delay and delete_after
|
|
select the LATE commit mode: extras are removed only after the whole
|
|
transfer has succeeded (plain --delete keeps this mode). The exact
|
|
semantics and the divergences from rsync are documented in RSYNC_COMPAT.md
|
|
and in config_delete_timing_early() below. */
|
|
bool delete_during;
|
|
bool delete_delay;
|
|
|
|
// PR #181: IPv6 and bind address
|
|
char* address;
|
|
char* bind_address;
|
|
bool ipv6;
|
|
bool ipv4;
|
|
/* --sockopts=OPTIONS (Phase 5, Wave B): strict allowlist of TCP/socket
|
|
* options applied via setsockopt after socket() and before connect()/bind().
|
|
* These are LOCAL socket concerns: they never cross the wire config frame.
|
|
* .address is the outgoing/source bind address (--address); .bind_address is
|
|
* reserved for daemon-side binding and is not wired yet. */
|
|
SockOptEntry* sockopts;
|
|
int sockopt_count;
|
|
|
|
// PR #182: Daemon/server mode
|
|
bool daemon;
|
|
char* daemon_config;
|
|
bool server_mode;
|
|
/* --no-motd (Wave C): CLIENT-ONLY, never crosses the wire. Suppresses
|
|
* DISPLAY of the daemon's MOTD; the daemon still sends the MOTD frame, so
|
|
* the client reads and discards it to keep the stream in sync. rsync's
|
|
* --no-motd is likewise a client-side display switch. Default false (the
|
|
* MOTD is shown when a daemon offers one). */
|
|
bool no_motd;
|
|
|
|
// PR #183: Checksum comparison
|
|
bool checksum;
|
|
|
|
// PR #184: Compression algorithm negotiation
|
|
char* compress_choice;
|
|
char* chmod_spec;
|
|
|
|
/* --checksum-choice / --cc and --checksum-seed. checksum_algo is the id of
|
|
* the whole-file content-digest algorithm used by the per-file --incremental
|
|
* handshake (sender computes it, receiver compares it to skip unchanged
|
|
* files) and by the basis-dir content verification. checksum_seed is passed
|
|
* to xxHash64 (and to the delta block strong hash, low 32 bits); md5 has no
|
|
* seed so it is ignored there. Both cross the wire: the receiver MUST hash
|
|
* the on-disk old file with the same algorithm and seed to reach a matching
|
|
* digest. Defaults (XXH64 / seed 0) reproduce the pre-existing behavior
|
|
* byte-for-byte. */
|
|
int checksum_algo; /* ChecksumAlgo, default CHECKSUM_ALGO_XXH64 */
|
|
uint64_t checksum_seed; /* default 0 */
|
|
|
|
char** skip_compress_suffixes;
|
|
int skip_compress_count;
|
|
bool skip_compress_set;
|
|
|
|
// Issue #131: Identity mapping. These configure whether and how the receiver
|
|
// applies ownership when it is actually preserved/applied. ALL of them cross
|
|
// the wire (protocol 2.11.0) so the receiver resolves and applies ownership
|
|
// with the exact policy the client requested. Plain -M/--preserve still does
|
|
// NOT apply ownership (FastSync's deliberate conservative default); it is
|
|
// only attempted when at least one of these is set (see identity.h).
|
|
/* --numeric-ids: no name lookup, use the transmitted numeric ids raw. */
|
|
bool numeric_ids;
|
|
/* --chown USER (owner) override; IDENTITY_CURRENT = the receiver's euid. */
|
|
bool chown_uid_set;
|
|
int32_t chown_uid;
|
|
/* --chown :GROUP (group) override; IDENTITY_CURRENT = the receiver's egid. */
|
|
bool chown_gid_set;
|
|
int32_t chown_gid;
|
|
/* --usermap / --groupmap entries, in order (first match wins). */
|
|
IdentityMap* usermap;
|
|
int usermap_count;
|
|
IdentityMap* groupmap;
|
|
int groupmap_count;
|
|
|
|
/* --super / --no-super (P7 Wave E, protocol 2.18.0): receiver-side privilege
|
|
* policy for super-user activities confined below the authorized receive
|
|
* root. SUPER_MODE_AUTO (default) preserves the pre-existing behavior: a
|
|
* privileged operation is only attempted when the receiver is ALREADY root
|
|
* (geteuid() == 0). SUPER_MODE_ON (--super) PERMITS the receiver to attempt
|
|
* those activities (ownership application, char/block device-node creation)
|
|
* even when it is not root -- the attempt is then confined exactly as before
|
|
* and simply fails/skips if the kernel refuses it. SUPER_MODE_OFF
|
|
* (--no-super) FORBIDS them even when running as root. FastSync NEVER
|
|
* elevates privileges (no setuid/seteuid/setgid) and never bypasses the
|
|
* fd-relative confinement (file_open_secure_parent, O_NOFOLLOW, root checks);
|
|
* --super only permits an attempt that is already confined. Crosses the wire
|
|
* as a trailing int so the receiver can enforce the policy. See
|
|
* privilege_super_permitted() in identity.h. */
|
|
int super_mode;
|
|
|
|
// Receiver-side runtime staging registry for --delay-updates. Never sent
|
|
// over the wire and never set on the sender side.
|
|
DelayUpdatesContext* delay_context;
|
|
|
|
// Phase 4: metadata time preservation. -U/--atimes and -N/--crtimes capture
|
|
// and transmit the source access / birth time (both sender and receiver
|
|
// effect, so they CROSS the wire). --omit-dir-times/-O and
|
|
// --omit-link-times/-J are receiver-side prefs (CROSS the wire). Their
|
|
// exact capture/transmit/apply semantics are documented in RSYNC_COMPAT.md.
|
|
/* -U/--atimes: preserve source access times on the destination. */
|
|
bool preserve_atimes;
|
|
/* -N/--crtimes: capture+transmit source birth time; see RSYNC_COMPAT for the
|
|
* receiver not-applied divergence. */
|
|
bool preserve_crtimes;
|
|
/* -O/--omit-dir-times: do not apply mtimes to directories. */
|
|
bool omit_dir_times;
|
|
/* -J/--omit-link-times: do not apply times to symlinks. */
|
|
bool omit_link_times;
|
|
/* --open-noatime: CLIENT-ONLY (never crosses the wire). The sender opens
|
|
* source files with O_NOATIME so reading for transfer does not bump the
|
|
* source access time. */
|
|
bool open_noatime;
|
|
|
|
// Phase 4: xattr / ACL / fake-super preservation.
|
|
/* -X/--xattrs and -A/--acls toggle the sender's capture and the receiver's
|
|
* application of per-file extended attributes (xattrs). Both cross the wire:
|
|
* the sender only transmits the bounded, whitelisted attribute set it
|
|
* captures and the receiver re-validates namespaces/sizes before applying
|
|
* fd-relative. With neither set (the default) no xattr block is sent, so the
|
|
* wire is byte-identical to prior protocol versions for unaffected runs. */
|
|
/* true when preserve_xattrs || preserve_acls; the sender/receiver gate the
|
|
* xattr wire block on this single flag. */
|
|
bool use_xattrs;
|
|
/* --fake-super: receiver-only. When set, each written file additionally gets
|
|
* a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime
|
|
* so a later privileged restore could re-apply them. Crosses the wire. */
|
|
bool fake_super;
|
|
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset
|
|
* implementation, a documented divergence from rsync's real identity switch:
|
|
* the receiver does NOT change its process credentials (FastSync's receiver
|
|
* is multithreaded, so a setuid/seteuid drop would be unsafe). Instead the
|
|
* receiver FORCES the ownership of every entry it writes to copy_as_uid /
|
|
* copy_as_gid through the existing confined, fd-relative identity path
|
|
* (fchown/fchownat), which REQUIRES receiver privilege (root); an
|
|
* unprivileged receiver REFUSES the whole transfer up front at the config
|
|
* handshake (never a silent wrong-ownership result). All three fields CROSS
|
|
* the wire as a trailing config-frame block so the receiver learns the
|
|
* requested ids; see the PROTOCOL_VERSION note below. */
|
|
bool copy_as_set;
|
|
int32_t copy_as_uid;
|
|
int32_t copy_as_gid;
|
|
|
|
// Phase 5: --trust-sender
|
|
/* Long-form-only, receiver-local policy. rsync's --trust-sender tells the
|
|
* receiving side to trust that the sender already produced a sane file list,
|
|
* relaxing the receiver's own up-front re-validation of every incoming path.
|
|
* In FastSync the receiver normally double-checks each transmitted file-list
|
|
* entry (empty / ".." path-traversal rejection) and refuses to materialize a
|
|
* symlink whose target could escape the receive root. When trust_sender is
|
|
* set, those redundant list-level re-checks are SKIPPED: the receiving side
|
|
* trusts the sender's list instead of re-validating it (fewer checks, faster,
|
|
* potentially unsafe, matching rsync). It is a LOCAL receiver policy and is
|
|
* NEVER serialized into the config frame (it exists only on the process that
|
|
* actually receives the file list). Even under trust_sender the low-level
|
|
* fd-relative confinement primitives (file_open_secure_parent, the O_NOFOLLOW
|
|
* parent walk, leaf/destination confinement) are deliberately KEPT as a hard
|
|
* floor, so a hostile sender still cannot write or link outside the
|
|
* authorized root (see the phase-5 notes in RSYNC_COMPAT.md). Off by
|
|
* default; only relaxes validation when explicitly requested. */
|
|
bool trust_sender;
|
|
|
|
// Phase 6: --stop-after / --stop-at
|
|
/* Client-only sender-side transfer stop deadlines. --stop-after=MINS stops
|
|
* the transfer after a number of elapsed minutes (checked against
|
|
* CLOCK_MONOTONIC so clock changes do not skew it); --stop-at=TIME stops at
|
|
* an absolute wall-clock time (HH:MM, HH:MM:SS, or now+N[smhd]). At the
|
|
* deadline the run stops elegantly at the next chunk/file boundary and the
|
|
* completion tail still runs (exit 0). Both are LOCAL to the sending
|
|
* process and are NEVER serialized into the config frame. */
|
|
int stop_after_mins; /* --stop-after=MINS minutes; 0 when unset */
|
|
time_t stop_at; /* --stop-at=... absolute wall-clock deadline */
|
|
bool stop_at_set; /* true when --stop-at was given */
|
|
|
|
// Phase 6: --write-batch / --only-write-batch / --read-batch
|
|
/* Client-only residual-batch paths. A residual batch is a self-contained
|
|
* single-file record of the whole source tree (full file images using the
|
|
* chunk codec), independent of any live server. --write-batch=FILE runs the
|
|
* normal live transfer AND additionally emits the batch FILE;
|
|
* --only-write-batch=FILE emits FILE only (no destination, no server);
|
|
* --read-batch=FILE applies FILE to the destination (no source, no server).
|
|
* All three are LOCAL to the driving process and are NEVER serialized into
|
|
* the config frame (the batch paths bypass the transport entirely). */
|
|
char* write_batch; /* --write-batch=FILE path, or NULL */
|
|
char* only_write_batch; /* --only-write-batch=FILE path, or NULL */
|
|
char* read_batch; /* --read-batch=FILE path, or NULL */
|
|
} Config;
|
|
|
|
/* Phase 5 (remote-option wave): 2.13.0 -> 2.14.0.
|
|
*
|
|
* WHY the bump, grounded in the wire: the binary config-frame layout is
|
|
* UNCHANGED by this wave (neither --remote-option nor --trust-sender adds a
|
|
* serialized field; see the field comments above). --remote-option is
|
|
* forwarded to the remote server over the SSH remote-command line
|
|
* (ssh_build_remote_command) and --trust-sender is a purely local receiver
|
|
* policy, so there is no new frame byte to negotiate. The bump is still the
|
|
* correct release marker for Phase 5 because the client-to-server INVOCATION
|
|
* surface changed: a client that composes remote-options expects a server that
|
|
* knows how to honor them, and the only safe way to express "this feature set
|
|
* is one coordinated release" is the strict same-version handshake FastSync
|
|
* already performs for every release. A 2.14 client against a 2.13 server
|
|
* fails the version check cleanly up front (rather than the remote server
|
|
* rejecting an unfamiliar forwarded argv at a confusing later point), which is
|
|
* exactly what the lockstep convention of this project requires. */
|
|
/* Daemon Wave A: 2.14.0 -> 2.15.0.
|
|
*
|
|
* WHY the bump, grounded in the wire: this wave really does add a serialized
|
|
* field to the binary config frame. The client sends its requested daemon
|
|
* module name (Config->module) as a new trailing string on the frame (sent
|
|
* after the Phase-4 xattr block and before the STATUS_OK/STATUS_ERROR ack, in
|
|
* config_send/config_receive), and the daemon reads it to select which module
|
|
* root confines the connection. Any config-frame layout change must bump the
|
|
* protocol version because a peer that does not parse the new trailing bytes
|
|
* would desynchronize on the frame boundary; the strict same-version handshake
|
|
* (config_receive rejects a mismatched version before parsing anything else)
|
|
* is what keeps a 2.15 client and a 2.14 server from ever reaching that state.
|
|
*
|
|
* NOTE: daemon module-selection bump owned by Wave A (2.15.0); later daemon
|
|
* waves (auth, motd) must not bump PROTOCOL_VERSION. Wave B (auth) adds the
|
|
* credential fields (auth_user/auth_password_hash) as further trailing
|
|
* config-frame strings AFTER the Wave A module string, with a presence int
|
|
* prefix. This is not a new frame version: sender and receiver of a 2.15.0
|
|
* build always read and write the same full layout (the strict same-version
|
|
* handshake rejects any other version before a byte of the frame is parsed),
|
|
* so a peer can never desynchronize on the added tail. The 2.15.0 release
|
|
* ships Wave A + Wave B together; the bump stays owned by Wave A.
|
|
*
|
|
* Wave C (MOTD) adds NO config-frame field and no version bump either. On the
|
|
* daemon listener path only, the server sends one MOTD string frame AFTER the
|
|
* config-frame STATUS_OK (server.c handler), and every 2.15.0 daemon client
|
|
* reads that frame right after the ack (client_send.c) -- symmetric
|
|
* server->client in every build, so the strict same-version handshake keeps the
|
|
* two peers in lockstep and nothing can desynchronize. The --stdio SSH path
|
|
* sends/reads no MOTD at all.
|
|
*
|
|
* --iconv Wave (P6): 2.15.0 -> 2.16.0.
|
|
*
|
|
* WHY the bump, grounded in the wire: the --iconv feature adds a serialized
|
|
* field to the binary config frame. The client sends the full CONVERT_SPEC
|
|
* (Config->iconv_spec) as a new trailing string AFTER the Wave A/B daemon-auth
|
|
* block (in config_send/config_receive), so the receiver knows the wire charset
|
|
* (the REMOTE half) before the first file name arrives. Any config-frame
|
|
* layout change must bump the protocol version: a peer that does not parse the
|
|
* new trailing bytes would desynchronize on the frame boundary, and the strict
|
|
* same-version handshake (config_receive rejects a mismatched version before
|
|
* parsing anything else) is what keeps a 2.16 client and a 2.15 server from
|
|
* ever reaching that state.
|
|
*
|
|
* Times Wave (P7 Wave D): 2.16.0 -> 2.17.0.
|
|
*
|
|
* WHY the bump, grounded in the wire: this wave makes -O/--omit-dir-times and
|
|
* -J/--omit-link-times REAL by adding directory and symlink time preservation.
|
|
* The config-frame LAYOUT is unchanged (the omit flags already crossed the
|
|
* wire), but the FRAME STREAM gains a new terminal frame: after all file data
|
|
* and the optional delete manifest, the sender transmits STATUS_DIR_TIMES
|
|
* frame(s) (each a count followed by (path, metadata) pairs, chunked so no
|
|
* frame exceeds the receiver's MAX_MANIFEST_ENTRIES bound) carrying every
|
|
* source directory's captured times, so the receiver can apply them AFTER all of a
|
|
* directory's children have been written (writing a child bumps the parent's
|
|
* mtime). Symlink entries already carry their metadata on the STATUS_SYMLINK
|
|
* frame; the receiver now applies it (utimensat/lchown with
|
|
* AT_SYMLINK_NOFOLLOW) unless -J is set. Any change to the frame sequence must
|
|
* bump the protocol version: a 2.16 peer that does not know STATUS_DIR_TIMES
|
|
* would desynchronize on the unknown frame, and the strict same-version
|
|
* handshake (config_receive rejects a mismatched version before parsing
|
|
* anything else) is what keeps a 2.17 client and a 2.16 server from ever
|
|
* reaching that state.
|
|
*
|
|
* Privilege Wave (P7 Wave E): 2.17.0 -> 2.18.0.
|
|
*
|
|
* WHY the bump, grounded in the wire: this wave adds the receiver-side
|
|
* privilege flags --super/--no-super and --copy-as=USER[:GROUP]. The
|
|
* config-frame layout gains two new trailing blocks AFTER the --iconv
|
|
* CONVERT_SPEC string, in this fixed order: (1) send_privilege_options /
|
|
* receive_privilege_options send one int (Config->super_mode, 0..2), then
|
|
* (2) send_copy_as_options / receive_copy_as_options send a presence int and,
|
|
* when set, the target uid and gid (both int32). The receiver uses
|
|
* super_mode to decide whether it may attempt super-user activities
|
|
* (ownership application, char/block device-node creation) already confined
|
|
* below the authorized receive root, and the copy-as ids to force the
|
|
* ownership of every entry it writes (the safe-subset --copy-as model). The
|
|
* receiver REQUIRES privilege for copy-as: an unprivileged receiver refuses
|
|
* the transfer at the config handshake (server_module_gate) instead of silently
|
|
* ignoring the flag. Any config-frame layout change must bump the protocol
|
|
* version: a peer that does not parse the new trailing bytes would
|
|
* desynchronize on the frame boundary, and the strict same-version handshake
|
|
* (config_receive rejects a mismatched version before parsing anything else) is
|
|
* what keeps a 2.18 client and a 2.17 server from ever reaching that state.
|
|
* --super never elevates privileges; it only permits a confined attempt, and
|
|
* --copy-as never switches process credentials (see RSYNC_COMPAT.md). */
|
|
#define PROTOCOL_VERSION "2.18.0"
|
|
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
|
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
|
#define MAX_BASIS_DIRS 64
|
|
|
|
/* Identity-mapping sentinels and bounds (see identity.h for semantics).
|
|
* IDENTITY_MATCH_ANY is a usermap/groupmap FROM '*' (matches any id);
|
|
* IDENTITY_CURRENT is a chown / map TO '*' (resolve to the receiver's current
|
|
* euid/egid at apply time). */
|
|
#define IDENTITY_MATCH_ANY (-1)
|
|
#define IDENTITY_CURRENT (-1)
|
|
#define MAX_IDENTITY_MAP 128
|
|
|
|
/* --super / --no-super tri-state (Config->super_mode). AUTO (default) and ON
|
|
* both permit a confined super-user attempt (AUTO preserves FastSync's
|
|
* historical best-effort behavior; an unprivileged attempt is refused by the
|
|
* kernel and skipped per entry); OFF forbids the attempt even for root. See
|
|
* privilege_super_mode_permitted() in identity.h. */
|
|
#define SUPER_MODE_AUTO 0
|
|
#define SUPER_MODE_ON 1
|
|
#define SUPER_MODE_OFF 2
|
|
|
|
Config* config_create(void);
|
|
void config_delete(Config* config);
|
|
bool config_send(int file_descriptor, const Config* config);
|
|
Config* config_receive(int file_descriptor);
|
|
bool config_is_remote_dest(const char* s);
|
|
void config_parse_ssh_dest(Config* config);
|
|
|
|
/* Server-side config-frame gate (daemon module selection, Wave A). A server
|
|
* that needs to make an accept/reject decision about a received Config BEFORE
|
|
* it sends the STATUS_OK ack (so a rejected connection is refused cleanly with
|
|
* no data transferred) passes a callback here; it runs after the frame parses
|
|
* and validates but before the STATUS_OK/STATUS_ERROR ack. Return NULL to
|
|
* accept the connection; return a non-NULL message to reject it (the message
|
|
* is logged server-side and STATUS_ERROR is sent in place of STATUS_OK). The
|
|
* callback runs in the connection's own process, so it may set up per-module
|
|
* process state (e.g. the authorized root). context is an opaque caller
|
|
* pointer. */
|
|
typedef const char* (*ConfigValidateFunc)(const Config* config, void* context);
|
|
Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc validate,
|
|
void* context);
|
|
|
|
/* Daemon-destination (host::module[/path]) helpers, Wave A. config_is_remote_dest
|
|
* recognizes the ordinary rsync-style single-colon host:path form used by the
|
|
* SSH transport; config_is_daemon_dest recognizes the double-colon form that
|
|
* selects a daemon module over TCP. config_parse_transport_dest is the single
|
|
* entry point main() uses: it parses a :: destination as a daemon TCP
|
|
* destination (host -> server_host, module -> config->module, path ->
|
|
* receive_root_directory) and otherwise falls back to the existing SSH
|
|
* host:path handling. */
|
|
bool config_is_daemon_dest(const char* s);
|
|
/* Returns 1 when the destination was daemon syntax and was parsed, 0 when it
|
|
* is not daemon syntax (nothing changed), -1 on an invalid daemon destination
|
|
* (a message is logged and config is left untouched). */
|
|
int config_parse_daemon_dest(Config* config);
|
|
/* Returns 1/0/-1 mirroring config_parse_daemon_dest when the destination is
|
|
* daemon syntax; otherwise runs the existing SSH host:path parse and returns
|
|
* 0. */
|
|
int config_parse_transport_dest(Config* config);
|
|
|
|
/* True when the negotiated delete timing performs the extra-file deletion
|
|
* BEFORE the transfer data (--delete-before / --delete-during). The flag is
|
|
* a pure function of the config and is used identically on the sender (to pick
|
|
* the manifest-first frame order) and the receiver (to delete when the early
|
|
* manifest arrives). When false the deletion is committed only after the whole
|
|
* transfer succeeded (--delete / --delete-after / --delete-delay). */
|
|
bool config_delete_timing_early(const Config* config);
|
|
/* Delete-timing sanity: with deletion enabled at most one timing flag may be
|
|
* set (none = the default delete-after commit timing); without deletion no
|
|
* timing flag may be set (each timing flag implies --delete). */
|
|
bool config_has_valid_delete_timing(const Config* config);
|
|
/* True when at least one --compare-dest/--copy-dest/--link-dest was set. */
|
|
bool config_has_basis(const Config* config);
|
|
/* Append one basis-dir entry. Returns 0 on success, -1 on allocation failure. */
|
|
int config_basis_append(Config* config, BasisDestType type, const char* path);
|
|
/* Validate a client-provided basis-dir path (relative, confined, non-empty). */
|
|
bool config_basis_path_valid(const char* path);
|
|
|
|
/* Parse and validate a --sockopts=OPTIONS comma-separated "OPT=VAL" list into a
|
|
* malloc'd array of at most *out_count entries. Returns 0 on success (the
|
|
* caller takes ownership of *out), or -1 on the first invalid option name or
|
|
* value. Pure/static-analysis friendly: performs no socket calls, so it is
|
|
* directly unit-testable. */
|
|
int config_sockopts_parse(const char* spec, SockOptEntry** out, int* out_count);
|
|
|
|
#endif
|