Files
FastSync/src/shared/protocol.h
T

441 lines
25 KiB
C

#ifndef PROTOCOL_H
#define PROTOCOL_H
#include "data.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdatomic.h>
/* Maximum allowed string size for receive_str (64 KB) */
#define MAX_STRING_SIZE (64 * 1024)
/* Hard cap on the optional server->client rejection detail carried by
* STATUS_ERROR_DETAIL (protocol 2.21.0). A longer message is sliced to this
* many bytes before it is sent, so a peer can never be made to retain more than
* this for a rejection and the detail frame stays a small, fixed bound. */
#define MAX_ERROR_DETAIL_BYTES 4096
/* Hard cap on a client diagnostic forwarded over the STATUS_CLIENT_MSG channel
* (protocol 2.30.0, rsync's --stderr=client). The body is reused from the
* bounded-string wire helper and sliced to this many bytes before it is sent,
* so a peer can never be made to retain more than this per message. */
#define MAX_CLIENT_MSG_BYTES 4096
/* Maximum uncompressed file payload accepted by the receiver's whole-file
* paths. A single whole file is charged against the per-connection memory
* reservation (MAX_CONNECTION_MEMORY) and against the server allocation
* ceiling (MAX_SERVER_ALLOC), so this mirrors those 256 MB bounds rather than
* the older 64 MB chunk-era cap. Chunk-serialized payloads keep their own
* 64 MB cap (MAX_CHUNK_SIZE). */
#define MAX_RECEIVE_WHOLE_FILE_SIZE (256ULL * 1024 * 1024)
/* Maximum allowed data payload size for receive_data (whole-file bound) */
#define MAX_DATA_PAYLOAD_SIZE MAX_RECEIVE_WHOLE_FILE_SIZE
/* Runtime whole-file receive bound. It defaults to MAX_RECEIVE_WHOLE_FILE_SIZE
* and exists so the test suite can lower the ceiling (via the
* FASTSYNC_MAX_WHOLE_FILE_SIZE environment variable, a byte count) and exercise
* the streaming path with a small, fast transfer. A payload at or below the
* bound keeps the historical whole-buffer path; a larger one is streamed
* through a bounded buffer. The value is resolved once per process and never
* exceeds the compile-time ceiling, so a malicious environment cannot raise it
* beyond the protocol limit. */
unsigned long long protocol_whole_file_receive_limit(void);
/* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */
#define MAX_CHUNK_SIZE (64ULL * 1024 * 1024)
/* Files larger than this are not kept fully in memory while loading: the
* loader skips them so the sender streams from the path, and file_checksum
* hashes them from disk in bounded buffers instead of forcing a full load. */
#define STREAM_THRESHOLD (64ULL * 1024 * 1024)
#define MAX_MANIFEST_ENTRIES (1024 * 1024)
/* Aggregate bytes retained by one received deletion manifest. */
#define MAX_MANIFEST_BYTES (16ULL * 1024 * 1024)
#define DEFAULT_MAX_ALLOC (1ULL * 1024 * 1024 * 1024)
/* Server policy ceiling for a client-provided allocation limit. */
#define MAX_SERVER_ALLOC (256ULL * 1024 * 1024)
/* Server-owned floor for the per-message I/O deadline. A client --timeout=0
(rsync's default) disables the client's own deadlines, but a server session
must never be held open forever by a silent peer (slow-loris), so the server
floors the effective deadline at this value. */
#define SERVER_IO_TIMEOUT_SEC 60
/* Bounded cumulative per-connection receive budget. In-flight wire buffers,
decompression buffers and queued (not yet written) file payloads for a
connection must stay within this ceiling. */
#define MAX_CONNECTION_MEMORY (256ULL * 1024 * 1024)
typedef struct ssl_st SSL;
typedef struct ProtocolSession ProtocolSession;
/*
* Transport vtable: the per-session set of I/O primitives the three protocol
* loops (send, receive, status-read) dispatch through. The ops are selected
* once, when the session is initialized or its SSL is installed, so the loops
* never branch on the transport at runtime. A plaintext session uses the
* read()/write() ops; a TLS session uses the SSL_read()/SSL_write() ops.
*
* `send`/`recv` attempt exactly one transfer and return:
* > 0 bytes transferred,
* PROTOCOL_IO_RETRY no progress; poll on *wait_events and retry,
* PROTOCOL_IO_CLOSED peer closed the stream,
* PROTOCOL_IO_ERROR fatal transport error.
* `has_pending` reports bytes already buffered by the transport (a TLS record
* residue); the receive loops skip the poll() gate when it is true.
*/
typedef struct ProtocolIoOps {
ssize_t (*send)(ProtocolSession* session, const void* data, size_t size, short* wait_events);
ssize_t (*recv)(ProtocolSession* session, void* data, size_t size, short* wait_events);
bool (*has_pending)(const ProtocolSession* session);
} ProtocolIoOps;
/* Negative sentinels returned by ProtocolIoOps.send/recv (see above). */
enum {
PROTOCOL_IO_RETRY = -1,
PROTOCOL_IO_CLOSED = -2,
PROTOCOL_IO_ERROR = -3,
};
/*
* Explicit owner of protocol I/O. A session does not own the descriptors or
* SSL object; it only describes the transport used by a transfer. This makes
* it safe to pass the transport to a worker without relying on inherited
* thread-local state.
*/
struct ProtocolSession {
int read_fd;
int write_fd;
SSL* ssl;
/* Transport dispatch selected by protocol_session_init()/set_ssl(). */
const ProtocolIoOps* ops;
unsigned long long bwlimit;
long long bw_tokens;
long long bw_last_refill_sec;
long bw_last_refill_nsec;
atomic_ullong total_allocated_bytes;
bool eight_bit_output;
unsigned long long max_alloc;
/* Per-session deadline (seconds) applied to every protocol send/receive by
* protocol_send_n_data / protocol_receive_n_data. The initialized default is
* the built-in 60 s window; a value <= 0 disables the deadline (rsync's
* --timeout=0). Set from the negotiated Config->timeout so --timeout is
* honored by the poll()-driven protocol I/O, not just the socket
* SO_RCVTIMEO/SO_SNDTIMEO. The server does not propagate a client 0 here: it
* installs protocol_server_io_timeout_sec() so its sessions keep a floor. */
int io_timeout_sec;
};
typedef int Status;
enum NET_STATUS {
STATUS_OK,
STATUS_ERROR,
STATUS_FINISHED,
STATUS_NEXT,
STATUS_CHUNK,
STATUS_MANIFEST,
STATUS_CHECK,
STATUS_DELTA_SIGNATURE,
STATUS_DELTA_DATA,
STATUS_KEEPALIVE,
STATUS_ABORT,
STATUS_CHECK_BATCH,
/* An explicit directory entry (--dirs / an empty source directory): the sender
* transmits the path and, when metadata/xattrs are negotiated, their blocks;
* the receiver creates the directory below the receive root. Protocol 2.30.0
* inserts an int32 probe flag right after the status when report_dest_info is
* negotiated: probe=1 is a report-only frame (path only; the receiver answers
* STATUS_DEST_INFO and creates nothing), probe=0 is a real create that is
* answered with the directory's pre-transfer state before it is created. */
STATUS_MKDIR,
/* --append / --append-verify tail resume. STATUS_APPEND is sent by the
* receiver after a per-file STATUS_CHECK when the existing destination file
* is SHORTER than the source and an append mode is negotiated: its payload is
* the resume offset (the number of prefix bytes already present), after which
* the sender answers either directly with STATUS_APPEND_DATA (plain --append,
* prefix not verified) or, for --append-verify, first with STATUS_APPEND_SIG
* carrying the xxHash64 of the source prefix; the receiver then replies
* STATUS_APPEND_OK (prefix matched -> sender transmits the tail) or
* STATUS_NEXT (prefix mismatch -> sender falls back to a full transfer).
* STATUS_APPEND_DATA carries the tail bytes (compressed data frame). */
STATUS_APPEND,
STATUS_APPEND_SIG,
STATUS_APPEND_OK,
STATUS_APPEND_DATA,
/* --hard-links/-H: a sibling (later member) of a source hard-link group.
* The sender transmits only the path, the run-local link-group id, and the
* first (data-carrying) member's destination-relative wire path; the receiver
* creates this entry as a hard link to the first member's installed file
* (falling back to a byte-identical copy if link() fails). Protocol 2.12.0. */
STATUS_HARDLINK,
/* A symlink-type entry (-l/--links, -k/--copy-dirlinks' keep-as-symlink
* branch). The sender transmits the destination path, the (sender-munged,
* if --munge-links) symlink target, and optional metadata; the receiver
* creates a symlink to the unmunged target beneath the receive root (see
* file_receive_symlink). Protocol 2.13.0. */
STATUS_SYMLINK,
/* --devices / --specials (-D): a device or special node the sender wants
* recreated (not written from content). Payload: destination path, the
* metadata frame (whose mode's S_IFMT bits carry the node kind), and two
* int32 rdev major/minor fields. The receiver validates the kind and rdev,
* confines the node below the receive root, and recreates it (mknod/mkfifo),
* privilege-gating the mknod. Protocol 2.13.0. */
STATUS_SPECIAL,
/* Directory-time superstructure (P7 Wave D, protocol 2.17.0): one or more
* trailing frames sent after all file data (and after the optional delete
* manifest) carrying the source directories' captured metadata so the
* receiver can apply directory mtimes/atimes AFTER all of a directory's
* children have been written. Payload per frame: an int count, then count
* repetitions of (wire path string, metadata frame); an entry count larger
* than MAX_MANIFEST_ENTRIES is split across repeated frames. The receiver
* defers the actual utimensat until its own delete/publish phase has
* committed, then skips the whole set when -O/--omit-dir-times is set. */
STATUS_DIR_TIMES,
/* Daemon SCRAM-SHA-256 authentication (A7 remediation, protocol 2.19.0).
* STATUS_AUTH_CHALLENGE: the server requires auth and is about to send the
* iteration count, the base64 salt and the base64 server nonce.
* STATUS_AUTH_RESPONSE: the client's reply, followed by the base64 client
* nonce and the base64 ClientProof. STATUS_AUTH_OK: the client proof
* verified, followed by the base64 ServerSignature. STATUS_AUTH_FAILED:
* a single generic refusal (unknown user, off-list user, wrong proof,
* missing/malformed credentials) after which the server closes without
* writing any data. */
STATUS_AUTH_CHALLENGE,
STATUS_AUTH_RESPONSE,
STATUS_AUTH_OK,
STATUS_AUTH_FAILED,
/* Optional server->client rejection detail (protocol 2.21.0). When the
* server refuses a transfer for a concrete reason it may send
* STATUS_ERROR_DETAIL followed by a length-prefixed, bounded string instead
* of a bare STATUS_ERROR. receive_status() consumes the string and maps the
* status back to STATUS_ERROR, so every pre-2.21 call site keeps working;
* callers that want the human-readable reason consult protocol_last_error().
* Appended immediately after STATUS_AUTH_FAILED so the existing wire values
* never move. */
STATUS_ERROR_DETAIL,
/* Server-contacting --dry-run (protocol 2.21.0). Sent by the receiver in
* response to a per-file STATUS_CHECK when the wire config carries
* dry_run=true and the file is NOT already up to date: it tells the sender
* the file WOULD be transferred, and the sender must NOT transmit any data
* (the receiver reads none in dry-run). STATUS_OK keeps its meaning in this
* path ("already up to date / nothing to do"). Appended after
* STATUS_ERROR_DETAIL so no existing status is renumbered. */
STATUS_DRY_RUN_TRANSFER,
/* --max-delete budget exhausted (protocol 2.23.0). Sent by the receiver as
* the terminal success status INSTEAD of STATUS_OK when a --delete/
* --delete-missing-args commit removed up to the --max-delete bound but had
* to skip further extras. The transfer itself succeeded and all file data is
* stored; the sender maps this to rsync's exit code 25 ("the --max-delete
* limit stopped deletions"). Appended after STATUS_DRY_RUN_TRANSFER so no
* existing status is renumbered. */
STATUS_DELETE_LIMIT,
/* Destination-state report for output parity (protocol 2.23.0; extended to
* directories/symlinks in 2.30.0). When the wire config carries
* report_dest_info=true, the receiver answers every per-file STATUS_CHECK
* request with STATUS_DEST_INFO FIRST, followed by a fixed record describing
* the pre-transfer destination entry (int32 has_old; int32 target_matches;
* uint64 size; int64 mtime; int64 mtime_nsec; uint32 mode; int32 uid;
* int32 gid). The ordinary STATUS_OK/STATUS_NEXT/... verdict follows, so the
* sender can render rsync-accurate -i/--out-format columns (new vs modified,
* and which of size/time/perms/owner/group differ) without changing the
* transfer decision itself. Protocol 2.30.0 also uses this record for
* STATUS_MKDIR and STATUS_SYMLINK: the sender consumes it into the entry's
* dest_state before emitting its change line, and target_matches reports
* whether an existing symlink's on-disk target already equals the incoming
* one (so the sender can render `cLc........` vs `.L..t......` and suppress
* an unchanged symlink). Appended after STATUS_DELETE_LIMIT so no existing
* status is renumbered. */
STATUS_DEST_INFO,
/* Per-directory delete plan (protocol 2.24.0). The sender of a
* --delete-during/--delete-delay transfer streams one frame per source
* directory in directory order instead of a single whole-tree keep-set
* manifest. The receiver applies the plan when it arrives
* (--delete-during removes that directory's extras immediately) or records
* the extras and applies them only after the whole transfer succeeded
* (--delete-delay). Payload: an int32 has_config flag (1 on the first plan
* of the run, 0 afterwards); when set, the three global config sections
* (protected-prefix count+paths, size-skipped count+paths, missing-args
* count+paths); then an int32 apply flag (1 for a real plan, 0 for a
* config-only carrier frame that must not walk a directory); then the
* destination-relative directory path wire string
* ("." for the receive root); then the child-directory count + names and the
* child-file count + names that must be kept. Appended after
* STATUS_DEST_INFO so no existing status is renumbered. */
STATUS_DELETE_PLAN,
/* End-of-transfer receiver counter report (protocol 2.25.0). When the wire
* config carries report_stats=true, the receiver sends this status once,
* immediately before its terminal success status, followed by a fixed stats
* record (see format_stats_send/receive in format.h) and, when the run is a
* --dry-run with --delete, the would-delete path list. Protocol 2.30.0
* appends the four deleted_reg/dir/link/special counters to that record, so
* --stats can render rsync's `Number of deleted files` per-type breakdown.
* Appended after STATUS_DELETE_PLAN so no existing status is renumbered. */
STATUS_STATS,
/* Client diagnostic channel (protocol 2.30.0, rsync's --stderr=client /
* --no-msgs2stderr). When the client's --stderr mode is `client`, the
* client forwards its own diagnostics over this client->server frame
* (STATUS_CLIENT_MSG followed by a bounded length-prefixed string, capped at
* MAX_CLIENT_MSG_BYTES) instead of writing them to its local stderr. The
* receiver reads the string and writes it to the server's stderr (respecting
* the server log destination). Appended after STATUS_STATS so no existing
* status is renumbered. */
STATUS_CLIENT_MSG,
/* Receiver-side partial transfer (protocol 2.30.0). Sent by the receiver as
* the terminal status INSTEAD of STATUS_OK when one or more entries failed
* per-entry without aborting the stream (currently a --devices mknod
* EPERM/EACCES). The transfer otherwise succeeded and every successfully
* stored file was acknowledged, so the sender may still remove
* --remove-source-files sources; the sender maps this to rsync's exit code
* 23 ("partial transfer due to error"), distinct from a fatal STATUS_ERROR.
* Appended after STATUS_CLIENT_MSG so no existing status is renumbered. */
STATUS_PARTIAL
};
void io_set_fds(int read_fd, int write_fd);
void io_set_bwlimit(unsigned long long bytes_per_sec);
unsigned long long io_get_bwlimit(void);
void io_set_ssl(SSL* ssl);
SSL* io_get_ssl(void);
/* SSL object of the transport in effect on this thread: the currently bound
* session's SSL when a TLS session is bound, otherwise the legacy thread-local
* io_ssl. NULL for a plaintext transport. Unlike io_get_ssl(), this resolves
* worker threads that bound a TLS session via protocol_session_set_ssl()/
* protocol_session_bind() but never called io_set_ssl() themselves (C11
* _Thread_local state is not inherited by a new thread). A bound session only
* wins when its selected dispatch is TLS; a bound plaintext session (ssl ==
* NULL) falls back to io_ssl so it can never mask a live encrypted transport.
* Callers that must choose a TLS-only code path (e.g. file_send.c's sendfile
* fallback) must use this instead of io_get_ssl(). */
SSL* protocol_current_ssl(void);
/* Process-wide wire byte counters. protocol_send_n_data/protocol_receive_n_data
* update them; the zero-copy sendfile path reports through
* protocol_note_bytes_written. Used by the client to render rsync's
* --stats/--progress totals and the --out-format %b/%c tokens. */
unsigned long long protocol_bytes_written(void);
unsigned long long protocol_bytes_read(void);
void protocol_note_bytes_written(unsigned long long bytes);
/* Apply --bwlimit pacing to bytes written outside protocol_send_n_data (the
* plaintext zero-copy sendfile fast path). `file_descriptor` is the wire fd
* the bytes were written to, so the legacy session is resolved exactly as the
* preceding send_n_data call resolved it (the bound TLS session still wins when
* set); resolving with the same fd avoids re-initializing the legacy session
* and granting a second first-call burst. Runs the same token-bucket throttle,
* so the sendfile transport is paced identically to the buffered/TLS paths. A
* no-op when the effective session has no bandwidth limit. */
void protocol_throttle_bytes(int file_descriptor, size_t bytes);
void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd);
/* Transitional bridge for helpers whose signatures still carry only an fd. */
void protocol_session_bind(ProtocolSession* session);
void protocol_session_unbind(void);
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl);
void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec);
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc);
/* Override the per-message send/receive deadline for this session. The value
* is stored verbatim: a positive value sets the deadline, `sec` <= 0 disables
* it (rsync's --timeout=0). An explicit long deadline (e.g. the delete-ack
* wait) is applied per-call by protocol_receive_status_timed and is unaffected
* by this setter. */
void protocol_session_set_io_timeout(ProtocolSession* session, int sec);
/* Effective per-message I/O deadline (seconds) for the currently-bound session.
* Zero means the deadline is disabled (rsync's --timeout=0). Used by the
* plaintext sendfile path which bypasses the protocol send primitive. */
int protocol_get_io_timeout_sec(void);
/* The server-side effective deadline for a client-requested timeout: a positive
* client value is honored, otherwise the SERVER_IO_TIMEOUT_SEC floor applies so
* a silent peer can never hold a session open forever. */
int protocol_server_io_timeout_sec(int client_timeout);
void* protocol_alloc(size_t size);
void* protocol_realloc(void* ptr, size_t size);
void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled);
void protocol_set_8_bit_output(bool enabled);
bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t data_size);
bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size);
bool protocol_send_str(ProtocolSession* session, const char* data);
char* protocol_receive_str(ProtocolSession* session);
/* Redacted string variants: identical wire framing to protocol_send_str /
* protocol_receive_str, but the payload body is replaced by `<redacted>` in the
* LOG_DEBUG_PROTO debug log. Used for daemon auth material (the username and
* the proof/signature fields) so a --verbose log can never capture a credential
* that could be replayed. */
bool protocol_send_str_redacted(ProtocolSession* session, const char* data);
char* protocol_receive_str_redacted(ProtocolSession* session);
bool protocol_send_data(ProtocolSession* session, const Data* data);
Data* protocol_receive_data_limited(ProtocolSession* session, unsigned long long maximum_size);
bool protocol_send_int(ProtocolSession* session, int data);
bool protocol_receive_int(ProtocolSession* session, int* data);
bool protocol_send_status(ProtocolSession* session, Status status);
bool protocol_receive_status(ProtocolSession* session, Status* status);
/* As protocol_receive_status, but with an explicit per-message deadline
* (seconds) instead of the session's configured io_timeout_sec. */
bool protocol_receive_status_timed(ProtocolSession* session, Status* status, int timeout_sec);
bool send_n_data(int file_descriptor, const void* data, size_t data_size);
bool receive_n_data(int file_descriptor, void* data, size_t data_size);
bool send_str(int file_descriptor, const char* data);
char* receive_str(int file_descriptor);
/* Redacted fd-level string variants (see protocol_send_str_redacted). */
bool send_str_redacted(int file_descriptor, const char* data);
char* receive_str_redacted(int file_descriptor);
bool send_data(int file_descriptor, const Data* data);
Data* receive_data(int file_descriptor);
Data* receive_data_limited(int file_descriptor, unsigned long long maximum_size);
/* Read exactly `size` bytes as a charged Data body. The length-prefixed
* receive_data_limited() reads the header itself; this variant is for callers
* that must inspect the declared size (and possibly stream the body instead)
* before allocating. `size` must already be within MAX_DATA_PAYLOAD_SIZE. */
Data* receive_data_body(int file_descriptor, unsigned long long size);
/* Allocate (and charge) a `size`-byte Data body without reading it; the caller
* fills `result->data` itself. Used when a frame's leading bytes must be
* inspected before the rest of the body is read. */
Data* receive_data_alloc(int file_descriptor, unsigned long long size);
bool send_int(int file_descriptor, int data);
bool receive_int(int file_descriptor, int* data);
bool send_status(int file_descriptor, Status status);
bool receive_status(int file_descriptor, Status* status);
/* Send STATUS_ERROR_DETAIL followed by a bounded (<= MAX_ERROR_DETAIL_BYTES)
* length-prefixed string. Over-long messages are sliced and NULL is treated
* as "". Returns false if the status or the string could not be sent. */
bool send_error_detail(int file_descriptor, const char* message);
/* Send STATUS_CLIENT_MSG followed by a bounded (<= MAX_CLIENT_MSG_BYTES)
* length-prefixed string carrying a client diagnostic. Over-long messages are
* sliced and NULL is treated as "". Returns false if the status or the string
* could not be sent. */
bool send_client_message(int file_descriptor, const char* message);
/* Human-readable reason captured from the most recent STATUS_ERROR_DETAIL
* received on this thread, or "" when the last status was a bare STATUS_ERROR
* (or no detail was seen). Thread-local, and valid until the next non-keepalive
* status read on the same thread; a later STATUS_KEEPALIVE does NOT clear it.
* The detail body is bounded by MAX_ERROR_DETAIL_BYTES: an over-cap declared
* length is drained and yields "" (so the stream never desyncs), while an
* absurd length is a fatal framing error that fails the status read. */
const char* protocol_last_error(void);
/* Clear the thread-local last-error buffer. */
void protocol_clear_last_error(void);
/* receive_status with an explicit per-message deadline in seconds, instead of
the default RECEIVE_TIMEOUT_SEC. A reply that may legitimately take longer
(e.g. the early-delete ACK after a large receiver-side deletion) must use
this so the sender does not abort after the deletion already committed. */
bool receive_status_timed(int file_descriptor, Status* status, int timeout_sec);
/* Callback polled by protocol_receive_status_keepalive once per keepalive
interval. Return true to stop waiting (e.g. a SIGINT/SIGTERM abort flag was
set). Kept as a function pointer so the protocol layer does not depend on
client signal state. */
typedef bool (*ProtocolWaitAbort)(void);
/* Like receive_status_timed, but while the peer is silent it emits
STATUS_KEEPALIVE every keepalive_interval_sec (the receiver answers each with
STATUS_KEEPALIVE, which this function consumes and skips) so a long
server-side operation does not look like a dead connection. The total wait
is still bounded by timeout_sec; abort_check (may be NULL) is polled every
interval and, when it returns true, ends the wait immediately with false.
Runs entirely on the calling thread: the protocol send path is NOT safe for
concurrent writers, so this must not be paired with a helper thread. */
bool receive_status_keepalive(int file_descriptor, Status* status, int timeout_sec,
int keepalive_interval_sec, ProtocolWaitAbort abort_check);
bool protocol_receive_status_keepalive(ProtocolSession* session, Status* status, int timeout_sec,
int keepalive_interval_sec, ProtocolWaitAbort abort_check);
#endif