feat(d5-daemon-motd): daemon MOTD display + --no-motd

This commit is contained in:
2026-09-10 13:52:24 +02:00
parent 8330f275a6
commit cf5f730940
14 changed files with 490 additions and 4 deletions
+7
View File
@@ -819,6 +819,13 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
config->no_implied_dirs = true;
continue;
}
/* "--no-motd" is a real rsync option name (client-side daemon MOTD display
* suppression), not a negation of a "--motd" flag, so it is handled before
* the generic --no-* negation branch. */
if (strcmp(argv[i], "--no-motd") == 0) {
config->no_motd = true;
continue;
}
if (strncmp(argv[i], "--no-", strlen("--no-")) == 0) {
if (strcmp(argv[i], "--no-delta") == 0)
no_delta = true;
+33
View File
@@ -11,6 +11,7 @@
#include "filter.h"
#include "hardlink.h"
#include "metadata.h"
#include "motd.h"
#include "log.h"
#include "multiprocessing.h"
#include "protocol.h"
@@ -359,6 +360,36 @@ static bool basis_oversize_preflight(const Config* config) {
return ok;
}
/* Read the daemon's MOTD frame and, unless --no-motd, display it on stdout.
*
* The daemon sends the MOTD as the first thing after the config-frame STATUS_OK
* on a host::module/path connection (rsync semantics), so this runs immediately
* after config_send succeeds. The frame is ALWAYS consumed for a daemon
* connection -- even with --no-motd -- so the byte stream stays in sync; the
* flag only suppresses the display. A non-daemon (local TCP / SSH) connection
* has no MOTD frame. The text is rendered through motd_render so a hostile
* server cannot inject terminal escape sequences. A read failure is not fatal
* here: the transfer that follows surfaces the real connection error. */
static void receive_daemon_motd(Client* client, const Config* config) {
if (!config->module || config->module[0] == '\0')
return;
char* motd = motd_receive(client->file_descriptor);
if (!motd)
return;
if (!config->no_motd && motd[0] != '\0') {
char* rendered = motd_render(motd, config->eight_bit_output);
if (rendered) {
fputs(rendered, stdout);
size_t length = strlen(rendered);
if (length == 0 || rendered[length - 1] != '\n')
fputc('\n', stdout);
fflush(stdout);
free(rendered);
}
}
free(motd);
}
/* Select the configured transport for both transfer execution paths. */
static Client* connect_transfer_client(const Config* config) {
if (config->transport == TRANSPORT_SSH) {
@@ -1350,6 +1381,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
protocol_session_unbind();
return thrd_error;
}
receive_daemon_motd(client, context->config);
if (context->early_delete) {
/* The keep-set manifest was prebuilt by a path-only pre-scan. Transmit it
and wait for the receiver to delete extras before streaming any data. */
@@ -1702,6 +1734,7 @@ int send_files(Config* config) {
memset(&prepared, 0, sizeof(prepared));
if (!config_send(client->file_descriptor, config))
goto send_fail;
receive_daemon_motd(client, config);
if (!prepare_scanner(config, 0, &prepared))
goto send_fail;
if (config->remove_source_files)
+2
View File
@@ -174,6 +174,8 @@ void print_usage(void) {
printf(" The file's first user:password line supplies the\n");
printf(" username and password (only a SHA-256 digest of the\n");
printf(" password is sent; keep the file mode 0600)\n");
printf(" --no-motd Suppress display of the daemon's MOTD (the server\n");
printf(" still sends it; the client just does not show it)\n");
printf(" --bwlimit <KB/s> Bandwidth limit in kilobytes per second\n");
printf(" --tls Enable TLS encryption\n");
printf(" --cert <path> TLS certificate file (PEM)\n");
+24
View File
@@ -5,6 +5,7 @@
#include "file.h"
#include "identity.h"
#include "log.h"
#include "motd.h"
#include "multiprocessing.h"
#include "protocol.h"
#include "queue.h"
@@ -365,6 +366,29 @@ void handler(int file_descriptor) {
during the whole transfer, and never bleeds across the per-connection
forked processes. Off by default. */
file_set_trust_sender(trust_sender);
/* Wave C MOTD: on the daemon listener path only, once the module gate + auth
have accepted and every destination check has passed, send the configured
`motd file` as the first server->client frame before any transfer data
(rsync sends its MOTD as the first thing from the server on a daemon
connection). Every daemon connection gets the frame -- an unset or
unreadable motd file sends an empty string -- so the client's read is
deterministic and an absent file is never an error. The --stdio SSH path
has no MOTD (g_daemon_conf is NULL there). No PROTOCOL_VERSION bump: the
frame is symmetric server->client in every 2.15.0 daemon build (see the
Wave C note in config.h). */
if (g_daemon_conf) {
char* motd = motd_read_file(g_daemon_conf->global.motd_file);
if (!motd_send(file_descriptor, motd ? motd : "")) {
free(motd);
log_message(LOG_LEVEL_ERROR, "Failed to send daemon MOTD");
config_delete(config);
close(file_descriptor);
protocol_session_unbind();
identity_clear_active();
return;
}
free(motd);
}
if (config->use_multithreading) {
Queue* q = queue_create(100, file_destroy);
if (q == NULL) {
+1
View File
@@ -149,6 +149,7 @@ static void config_set_defaults(Config* config) {
config->daemon = false;
config->daemon_config = NULL;
config->server_mode = false;
config->no_motd = false;
config->checksum = false;
config->checksum_algo = CHECKSUM_ALGO_XXH64;
config->checksum_seed = 0;
+15 -1
View File
@@ -340,6 +340,12 @@ typedef struct Config {
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;
@@ -479,7 +485,15 @@ typedef struct Config {
* 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. */
* 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. */
#define PROTOCOL_VERSION "2.15.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
+76
View File
@@ -0,0 +1,76 @@
#include "motd.h"
#include "protocol.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* motd_read_file(const char* path) {
if (!path || path[0] == '\0')
return NULL;
FILE* fp = fopen(path, "rb");
if (!fp)
return NULL;
char* buffer = malloc(MOTD_MAX_BYTES + 1);
if (!buffer) {
fclose(fp);
return NULL;
}
/* fread stops at the bound; a larger file is truncated rather than read
* unbounded. ferror distinguishes a truncated read from an I/O failure. */
size_t total = fread(buffer, 1, MOTD_MAX_BYTES, fp);
if (ferror(fp)) {
free(buffer);
fclose(fp);
return NULL;
}
fclose(fp);
buffer[total] = '\0';
return buffer;
}
char* motd_render(const char* motd, bool eight_bit_output) {
if (!motd)
return NULL;
size_t length = strlen(motd);
if (length > (SIZE_MAX - 1) / 5)
return NULL;
char* rendered = malloc(length * 5 + 1);
if (!rendered)
return NULL;
size_t out = 0;
for (size_t i = 0; i < length; i++) {
unsigned char byte = (unsigned char)motd[i];
if (byte == '\n' || byte == '\t') {
rendered[out++] = (char)byte;
} else if ((byte >= 32 && byte <= 126) || (eight_bit_output && byte >= 128)) {
rendered[out++] = (char)byte;
} else {
rendered[out++] = '\\';
rendered[out++] = '#';
rendered[out++] = (char)('0' + ((byte >> 6) & 7));
rendered[out++] = (char)('0' + ((byte >> 3) & 7));
rendered[out++] = (char)('0' + (byte & 7));
}
}
rendered[out] = '\0';
return rendered;
}
bool motd_send(int file_descriptor, const char* motd) {
return send_str(file_descriptor, motd ? motd : "");
}
char* motd_receive(int file_descriptor) {
char* motd = receive_str(file_descriptor);
if (!motd)
return NULL;
/* Guard against a hostile/oversized peer: receive_str already bounded the
* frame at MAX_STRING_SIZE and consumed it, so discarding an over-bound
* body here keeps the stream framed while refusing to display it. */
if (strlen(motd) > MOTD_MAX_BYTES) {
free(motd);
return NULL;
}
return motd;
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef MOTD_H
#define MOTD_H
#include <stdbool.h>
/* Daemon Message-Of-The-Day (Wave C).
*
* The daemon listener (fastsync-server --daemon) may advertise a `motd file`
* configured in its globals. When a client connects with a host::module/path
* destination and the module gate accepts the connection, the server sends the
* MOTD as a single string frame BEFORE any transfer data (rsync sends its MOTD
* as the first thing from the server at the start of a daemon connection).
* The client reads that frame right after the config/status handshake and
* displays it on stdout unless --no-motd was given.
*
* The MOTD is ordinary display text, never a secret, so it uses the normal
* (non-redacted) string primitive. The exchange is strictly server->client
* and happens on the daemon listener path only; the --stdio SSH path has no
* MOTD.
*
* No PROTOCOL_VERSION bump is involved: the frame is sent and read
* symmetrically by every 2.15.0 daemon build (the strict same-version
* handshake rejects any other version before the frame), so it cannot
* desynchronize a peer. */
/* Upper bound on the MOTD bytes the server will read from disk and put on the
* wire. Kept far below MAX_STRING_SIZE (64 KB) so a huge/hostile motd file
* can never produce an unbounded frame or allocation. */
#define MOTD_MAX_BYTES 4096
/* Read a daemon MOTD file, bounded to MOTD_MAX_BYTES. Returns a malloc'd
* NUL-terminated copy of the file content (bytes beyond the bound are
* truncated) or NULL when path is NULL/empty, the file cannot be opened or
* read, or allocation fails. An absent or unreadable motd file is NOT an
* error: the caller simply sends an empty MOTD frame and continues. */
char* motd_read_file(const char* path);
/* Render MOTD text for terminal display. Newlines and tabs are preserved so
* a multi-line motd still reads naturally, while every other non-printable /
* control byte (ESC included) is escaped with FastSync's `\NNN` octal
* convention, so a hostile server cannot inject terminal escape sequences
* through the MOTD. eight_bit_output keeps bytes >= 0x80 verbatim (matching
* --8-bit-output). Returns a malloc'd string or NULL on allocation failure. */
char* motd_render(const char* motd, bool eight_bit_output);
/* Send/receive the MOTD string frame. These wrap the normal string
* primitive: the MOTD is not a credential, so no redaction is used. The
* receiver additionally rejects an over-bound frame (> MOTD_MAX_BYTES) as a
* hostile input guard; the frame itself is always fully consumed first, so the
* stream stays framed. */
bool motd_send(int file_descriptor, const char* motd);
char* motd_receive(int file_descriptor);
#endif