Today a server rejection sends a bare STATUS_ERROR and the reason only
reaches the server log, so the client cannot say why a transfer was
refused. Add an optional, bounded server->client error-detail frame:
- Status gains STATUS_ERROR_DETAIL appended LAST so existing wire
values are unchanged.
- send_error_detail(fd, msg) sends STATUS_ERROR_DETAIL followed by the
existing length-prefixed string primitive, slicing over-long messages
to MAX_ERROR_DETAIL_BYTES (4096).
- receive_status() (and the timed/keepalive status readers) always
consume the detail body and map the status back to STATUS_ERROR,
capturing the text into a thread-local buffer exposed by
protocol_last_error(); a bare STATUS_ERROR leaves it cleared. Every
existing call site keeps working and the stream cannot desync.
- Upgrade the daemon module gate / config validation (config.c), the
final transfer failure (server.c) and receiver-side path/node
validation (file_receive.c) to send a concrete reason; surface it on
the client in client_send.c/config.c.
- Bump PROTOCOL_VERSION to 2.21.0 (CMake VERSION, CHANGELOG, docs) and
update the pinned config wire golden hash / CLI-version tests.
- Add tests/test_protocol_error.c covering mapping+capture, the
over-long bound, bare-error clearing, and thread-locality.
246 lines
13 KiB
C
246 lines
13 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
|
|
|
|
/* 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
|
|
|
|
/* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */
|
|
#define MAX_CHUNK_SIZE (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)
|
|
/* 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;
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
typedef struct ProtocolSession {
|
|
int read_fd;
|
|
int write_fd;
|
|
SSL* ssl;
|
|
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. Defaults to the built-in
|
|
* 60 s window; a value <= 0 falls back to that default. 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. */
|
|
int io_timeout_sec;
|
|
} ProtocolSession;
|
|
|
|
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): the sender transmits only the path;
|
|
* the receiver creates the directory below the receive root. */
|
|
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 last so the existing wire values never move. */
|
|
STATUS_ERROR_DETAIL
|
|
};
|
|
|
|
void io_set_fds(int read_fd, int write_fd);
|
|
void io_set_bwlimit(unsigned long long bytes_per_sec);
|
|
void io_set_ssl(SSL* ssl);
|
|
SSL* io_get_ssl(void);
|
|
|
|
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.
|
|
* `sec` <= 0 restores the built-in 60 s default (used for --timeout=0/unset).
|
|
* 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,
|
|
* falling back to the built-in default. Used by the plaintext sendfile path
|
|
* which bypasses the protocol send primitive. */
|
|
int protocol_get_io_timeout_sec(void);
|
|
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);
|
|
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);
|
|
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);
|
|
/* 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 status read
|
|
* on the same thread. */
|
|
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
|