Merge Wave 3b: configurable protocol timeout and idle/session bounds
CI / lint (push) Successful in 1m31s
CI / sanitizers (undefined) (push) Successful in 1m1s
CI / sanitizers (address) (push) Successful in 1m8s
CI / fuzz-build (push) Successful in 30s
CI / coverage (push) Successful in 50s
CI / build-and-test (push) Successful in 4m37s
CI / valgrind (push) Successful in 3m12s
CI / lint (push) Successful in 1m31s
CI / sanitizers (undefined) (push) Successful in 1m1s
CI / sanitizers (address) (push) Successful in 1m8s
CI / fuzz-build (push) Successful in 30s
CI / coverage (push) Successful in 50s
CI / build-and-test (push) Successful in 4m37s
CI / valgrind (push) Successful in 3m12s
This commit is contained in:
@@ -132,7 +132,7 @@ partial, alternate, and planned behavior.
|
|||||||
| `--existing` | Skip files not already present at the destination; update existing files normally. |
|
| `--existing` | Skip files not already present at the destination; update existing files normally. |
|
||||||
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
|
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
|
||||||
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
|
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
|
||||||
| `--timeout <sec>` | I/O timeout in seconds (default: 30) |
|
| `--timeout <sec>` | Positive I/O timeout in seconds, applied to both the socket (`SO_RCVTIMEO`/`SO_SNDTIMEO`, built-in default 30 s) and the per-message protocol poll deadline (built-in default 60 s). Omit the option to keep both built-ins; `0` is rejected. The server side keeps the built-in 60 s protocol window (the value is not sent on the wire). |
|
||||||
| `--contimeout <sec>` | Connection timeout in seconds (default: 10) |
|
| `--contimeout <sec>` | Connection timeout in seconds (default: 10) |
|
||||||
| `--backup` | Backup existing destination files before overwriting |
|
| `--backup` | Backup existing destination files before overwriting |
|
||||||
| `--backup-dir <dir>` | Target directory for backups (requires `--backup`) |
|
| `--backup-dir <dir>` | Target directory for backups (requires `--backup`) |
|
||||||
@@ -151,6 +151,19 @@ partial, alternate, and planned behavior.
|
|||||||
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
|
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
|
||||||
| `--client-cn <name>` | TLS client certificate common name; mandatory with `--tls` (a TLS connection always verifies the client CN) |
|
| `--client-cn <name>` | TLS client certificate common name; mandatory with `--tls` (a TLS connection always verifies the client CN) |
|
||||||
|
|
||||||
|
**Per-message vs. connection timeouts.** `--timeout` bounds each individual protocol
|
||||||
|
send/receive (the `poll()` deadline), so a peer that stops mid-frame is dropped. It
|
||||||
|
does not, by itself, stop a peer that keeps sending well-formed frames forever. The
|
||||||
|
receiver therefore also enforces two wall-clock (`CLOCK_MONOTONIC`) bounds on a
|
||||||
|
connection: a **1 hour** idle limit and a **24 hour** overall session cap. Only
|
||||||
|
frames that move real work (not `STATUS_KEEPALIVE`/`STATUS_ABORT` and not an
|
||||||
|
empty `STATUS_CHECK_BATCH`/`STATUS_DIR_TIMES`) refresh the idle timestamp, so a
|
||||||
|
peer cannot hold a connection slot by emitting cheap empty frames; a peer that
|
||||||
|
fabricates minimal non-empty frames can still occupy a slot until the 24 hour
|
||||||
|
cap, since no bound can require actual payload without risking a legitimate
|
||||||
|
long operation. Both are deliberately generous so a legitimate long-running
|
||||||
|
transfer is never aborted.
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
| Argument | Description |
|
| Argument | Description |
|
||||||
@@ -384,7 +397,7 @@ features without changing the meaning of ordinary compatibility options.
|
|||||||
| `--bwlimit <KB/s>` | Apply token-bucket bandwidth limiting. |
|
| `--bwlimit <KB/s>` | Apply token-bucket bandwidth limiting. |
|
||||||
| `--progress` | Show transfer progress and throughput. |
|
| `--progress` | Show transfer progress and throughput. |
|
||||||
| `--stats` | Print transfer statistics. |
|
| `--stats` | Print transfer statistics. |
|
||||||
| `--timeout <seconds>` | Set I/O timeout. |
|
| `--timeout <seconds>` | Set the socket **and** per-message protocol I/O timeout (positive seconds). Omit to keep the built-in 30 s socket / 60 s protocol defaults. |
|
||||||
| `--contimeout <seconds>` | Set connection timeout. |
|
| `--contimeout <seconds>` | Set connection timeout. |
|
||||||
|
|
||||||
Short-option conflicts with rsync have been resolved for the CLI namespace
|
Short-option conflicts with rsync have been resolved for the CLI namespace
|
||||||
|
|||||||
@@ -1435,6 +1435,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
|||||||
}
|
}
|
||||||
ProtocolSession session;
|
ProtocolSession session;
|
||||||
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
||||||
|
protocol_session_set_io_timeout(&session, context->config->timeout);
|
||||||
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
||||||
protocol_session_bind(&session);
|
protocol_session_bind(&session);
|
||||||
if (!config_send(client->file_descriptor, context->config)) {
|
if (!config_send(client->file_descriptor, context->config)) {
|
||||||
@@ -1896,6 +1897,7 @@ int send_files(Config* config) {
|
|||||||
}
|
}
|
||||||
ProtocolSession session;
|
ProtocolSession session;
|
||||||
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
||||||
|
protocol_session_set_io_timeout(&session, config->timeout);
|
||||||
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
||||||
protocol_session_bind(&session);
|
protocol_session_bind(&session);
|
||||||
int ret = 1;
|
int ret = 1;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) {
|
bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) {
|
||||||
if (!outcomes)
|
if (!outcomes)
|
||||||
@@ -153,6 +154,93 @@ static bool receiver_process_batch(Config* config, int file_descriptor) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Anti-slowloris connection bounds ----
|
||||||
|
* A legitimate transfer either streams data frames continuously or, when it
|
||||||
|
* must pause, sends STATUS_KEEPALIVE so the peer sees the connection is alive.
|
||||||
|
* An attacker can therefore squat on a connection slot indefinitely by sending
|
||||||
|
* only keepalives under the per-message timeout. Two CLOCK_MONOTONIC bounds
|
||||||
|
* defeat that without ever punishing a real transfer:
|
||||||
|
*
|
||||||
|
* MAX_SESSION_IDLE_SEC (1 h): the longest a stream may make no forward
|
||||||
|
* progress. Data/status frames count as progress and refresh the timer;
|
||||||
|
* keepalives do not. One hour is far longer than any real pause between
|
||||||
|
* data frames, yet small enough to reap a slowloris well before the 24 h
|
||||||
|
* session cap.
|
||||||
|
*
|
||||||
|
* MAX_SESSION_WALL_SEC (24 h): an absolute ceiling on one connection's
|
||||||
|
* lifetime as defense-in-depth against a trickle of progress frames that
|
||||||
|
* resets the idle timer just below its limit. Larger than any plausible
|
||||||
|
* single transfer while still bounding resource occupancy.
|
||||||
|
*
|
||||||
|
* Both are wall-clock deltas, so the per-message poll timeout (60 s by default,
|
||||||
|
* or --timeout) can never fool them, and both the single-threaded and the -m
|
||||||
|
* receiver paths (receiver_process_pending) share the same logic. */
|
||||||
|
#define MAX_SESSION_IDLE_SEC 3600u
|
||||||
|
#define MAX_SESSION_WALL_SEC 86400u
|
||||||
|
|
||||||
|
static unsigned int g_max_session_idle_sec = MAX_SESSION_IDLE_SEC;
|
||||||
|
static unsigned int g_max_session_wall_sec = MAX_SESSION_WALL_SEC;
|
||||||
|
|
||||||
|
void receiver_set_time_limits(unsigned int idle_sec, unsigned int wall_sec) {
|
||||||
|
g_max_session_idle_sec = idle_sec;
|
||||||
|
g_max_session_wall_sec = wall_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void receiver_reset_time_limits(void) {
|
||||||
|
g_max_session_idle_sec = MAX_SESSION_IDLE_SEC;
|
||||||
|
g_max_session_wall_sec = MAX_SESSION_WALL_SEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool receiver_time_limit_exceeded(const struct timespec* session_start,
|
||||||
|
const struct timespec* last_progress,
|
||||||
|
const struct timespec* now) {
|
||||||
|
if (!session_start || !last_progress || !now)
|
||||||
|
return false;
|
||||||
|
if (now->tv_sec - session_start->tv_sec >= (time_t)g_max_session_wall_sec)
|
||||||
|
return true;
|
||||||
|
if (now->tv_sec - last_progress->tv_sec >= (time_t)g_max_session_idle_sec)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A frame proves forward progress only when it cannot be fabricated for free.
|
||||||
|
* KEEPALIVE/ABORT are pure liveness, and CHECK_BATCH/DIR_TIMES may carry zero
|
||||||
|
* entries, so a peer must not be able to hold a connection slot forever by
|
||||||
|
* merely emitting empty frames. */
|
||||||
|
static bool status_counts_as_progress(Status status) {
|
||||||
|
switch (status) {
|
||||||
|
case STATUS_KEEPALIVE:
|
||||||
|
case STATUS_ABORT:
|
||||||
|
case STATUS_CHECK_BATCH:
|
||||||
|
case STATUS_DIR_TIMES:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Refresh the progress timestamp for a forward-moving frame and enforce the
|
||||||
|
* bounds above. Returns false when the connection must be dropped; the
|
||||||
|
* terminal STATUS_ERROR is sent only when the sink owns error reporting (the
|
||||||
|
* -m sink sets send_error=false so the main thread emits exactly one). */
|
||||||
|
static bool receiver_note_status(const struct timespec* session_start,
|
||||||
|
struct timespec* last_progress, Status status, int file_descriptor,
|
||||||
|
const ReceiverSink* sink) {
|
||||||
|
struct timespec now;
|
||||||
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
|
||||||
|
now = *last_progress;
|
||||||
|
if (status_counts_as_progress(status))
|
||||||
|
*last_progress = now;
|
||||||
|
if (!receiver_time_limit_exceeded(session_start, last_progress, &now))
|
||||||
|
return true;
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"Receive session exceeded its time bound (idle %us / total %us); aborting connection",
|
||||||
|
g_max_session_idle_sec, g_max_session_wall_sec);
|
||||||
|
if (!sink || sink->send_error)
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
|
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
|
||||||
return receiver_process_pending(config, file_descriptor, sink, NULL);
|
return receiver_process_pending(config, file_descriptor, sink, NULL);
|
||||||
}
|
}
|
||||||
@@ -172,6 +260,15 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
Status status;
|
Status status;
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
return -1;
|
return -1;
|
||||||
|
/* Wall-clock (=CLOCK_MONOTONIC) anti-slowloris bookkeeping. session_start is
|
||||||
|
* fixed for the whole connection; last_progress is refreshed by every frame
|
||||||
|
* that is not a keepalive/abort. */
|
||||||
|
struct timespec session_start;
|
||||||
|
struct timespec last_progress;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &session_start);
|
||||||
|
last_progress = session_start;
|
||||||
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
||||||
|
return -1;
|
||||||
bool early_delete = config_delete_timing_early(config);
|
bool early_delete = config_delete_timing_early(config);
|
||||||
/* Parked keep-set for the late/commit timing. Every exit path below frees it
|
/* Parked keep-set for the late/commit timing. Every exit path below frees it
|
||||||
exactly once; the only exception is the successful FINISHED handoff, which
|
exactly once; the only exception is the successful FINISHED handoff, which
|
||||||
@@ -271,6 +368,8 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
next_status:
|
next_status:
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
goto receive_error;
|
goto receive_error;
|
||||||
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
||||||
|
goto fail;
|
||||||
}
|
}
|
||||||
if (status != STATUS_FINISHED) {
|
if (status != STATUS_FINISHED) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "file_receive.h"
|
#include "file_receive.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
typedef bool (*ReceiverFileSink)(File* file, void* context);
|
typedef bool (*ReceiverFileSink)(File* file, void* context);
|
||||||
|
|
||||||
@@ -45,4 +47,22 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
DeleteManifest** pending_manifest);
|
DeleteManifest** pending_manifest);
|
||||||
int receiver_receive_files(Config* config, int file_descriptor);
|
int receiver_receive_files(Config* config, int file_descriptor);
|
||||||
|
|
||||||
|
/* ---- Connection time bounds (anti-slowloris) ----
|
||||||
|
* receiver_process_pending() aborts a connection that makes no forward progress
|
||||||
|
* (only STATUS_KEEPALIVE/STATUS_ABORT frames) beyond a wall-clock idle limit,
|
||||||
|
* and enforces a hard cap on the whole session. Both are CLOCK_MONOTONIC
|
||||||
|
* deltas, independent of the per-message poll deadline, so a 60 s (or
|
||||||
|
* --timeout) receive window can never reset them. Defaults are deliberately
|
||||||
|
* generous (see MAX_SESSION_IDLE_SEC / MAX_SESSION_WALL_SEC in receiver.c). */
|
||||||
|
|
||||||
|
/* Test seam: override the idle/session wall-clock limits (0 = abort on the
|
||||||
|
* next status). Always restore with receiver_reset_time_limits(). */
|
||||||
|
void receiver_set_time_limits(unsigned int idle_sec, unsigned int wall_sec);
|
||||||
|
void receiver_reset_time_limits(void);
|
||||||
|
/* Pure predicate over explicit monotonic timestamps, exposed so the bound is
|
||||||
|
* unit-testable without sleeping. True when either the idle or the overall
|
||||||
|
* session limit has elapsed. */
|
||||||
|
bool receiver_time_limit_exceeded(const struct timespec* session_start,
|
||||||
|
const struct timespec* last_progress, const struct timespec* now);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -609,6 +609,12 @@ void handler(int file_descriptor) {
|
|||||||
if (gate_ctx.super_mode_override != -1)
|
if (gate_ctx.super_mode_override != -1)
|
||||||
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
||||||
protocol_set_8_bit_output(config->eight_bit_output);
|
protocol_set_8_bit_output(config->eight_bit_output);
|
||||||
|
/* Server-side per-message protocol deadline for every frame from here on.
|
||||||
|
* `timeout` is not serialized, so this is the server's own config (the server
|
||||||
|
* has no --timeout CLI and defaults it to 0): the built-in 60 s window stays
|
||||||
|
* in effect. A client's --timeout tightens only that client's own protocol
|
||||||
|
* I/O and the server's socket read/write timeout is the transport default. */
|
||||||
|
protocol_session_set_io_timeout(&session, config->timeout);
|
||||||
if (!authorized_root) {
|
if (!authorized_root) {
|
||||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||||
goto done;
|
goto done;
|
||||||
@@ -743,6 +749,7 @@ void handler(int file_descriptor) {
|
|||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
protocol_session_set_max_alloc(&context->session, config->max_alloc);
|
protocol_session_set_max_alloc(&context->session, config->max_alloc);
|
||||||
|
protocol_session_set_io_timeout(&context->session, config->timeout);
|
||||||
atomic_store(&context->session.total_allocated_bytes,
|
atomic_store(&context->session.total_allocated_bytes,
|
||||||
atomic_load(&session.total_allocated_bytes));
|
atomic_load(&session.total_allocated_bytes));
|
||||||
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
|
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
|
||||||
|
|||||||
+5
-1
@@ -67,7 +67,11 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->tls_ca = NULL;
|
config->tls_ca = NULL;
|
||||||
config->server_host = str_dup("127.0.0.1");
|
config->server_host = str_dup("127.0.0.1");
|
||||||
config->server_port = 8080;
|
config->server_port = 8080;
|
||||||
config->timeout = 30;
|
/* 0 means "--timeout not given": the transport keeps its own built-in 30 s
|
||||||
|
* socket timeout (tcp_set_timeouts ignores non-positive values) and the
|
||||||
|
* protocol layer keeps its built-in 60 s per-message deadline. A positive
|
||||||
|
* value overrides BOTH (see protocol_session_set_io_timeout). */
|
||||||
|
config->timeout = 0;
|
||||||
config->contimeout = 10;
|
config->contimeout = 10;
|
||||||
config->quiet = false;
|
config->quiet = false;
|
||||||
config->backup = false;
|
config->backup = false;
|
||||||
|
|||||||
@@ -157,7 +157,12 @@ typedef struct Config {
|
|||||||
char* tls_cert;
|
char* tls_cert;
|
||||||
char* tls_key;
|
char* tls_key;
|
||||||
char* tls_ca;
|
char* tls_ca;
|
||||||
|
/* --timeout: per-message I/O deadline in seconds. 0 (the default/unset
|
||||||
|
* sentinel) leaves the transport's built-in 30 s socket timeout and the
|
||||||
|
* protocol's built-in 60 s per-message deadline in place; a positive value
|
||||||
|
* overrides both. See protocol_session_set_io_timeout. */
|
||||||
int timeout;
|
int timeout;
|
||||||
|
/* --contimeout: connect()/accept timeout, transport layer only. */
|
||||||
int contimeout;
|
int contimeout;
|
||||||
bool quiet;
|
bool quiet;
|
||||||
bool backup;
|
bool backup;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
|
|||||||
off_t offset = 0;
|
off_t offset = 0;
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
deadline.tv_sec += 60;
|
deadline.tv_sec += protocol_get_io_timeout_sec();
|
||||||
while ((unsigned long long)offset < file_size) {
|
while ((unsigned long long)offset < file_size) {
|
||||||
struct timespec now;
|
struct timespec now;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
|||||||
+19
-2
@@ -76,10 +76,23 @@ void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd)
|
|||||||
session->read_fd = read_fd;
|
session->read_fd = read_fd;
|
||||||
session->write_fd = write_fd;
|
session->write_fd = write_fd;
|
||||||
session->max_alloc = DEFAULT_MAX_ALLOC;
|
session->max_alloc = DEFAULT_MAX_ALLOC;
|
||||||
|
session->io_timeout_sec = RECEIVE_TIMEOUT_SEC;
|
||||||
atomic_init(&session->total_allocated_bytes, 0);
|
atomic_init(&session->total_allocated_bytes, 0);
|
||||||
protocol_session_set_bwlimit(session, global_bwlimit());
|
protocol_session_set_bwlimit(session, global_bwlimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void protocol_session_set_io_timeout(ProtocolSession* session, int sec) {
|
||||||
|
if (!session)
|
||||||
|
return;
|
||||||
|
session->io_timeout_sec = sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
int protocol_get_io_timeout_sec(void) {
|
||||||
|
const ProtocolSession* session = bound_session ? bound_session : &legacy_io_session;
|
||||||
|
int sec = session->io_timeout_sec;
|
||||||
|
return sec > 0 ? sec : RECEIVE_TIMEOUT_SEC;
|
||||||
|
}
|
||||||
|
|
||||||
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc) {
|
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc) {
|
||||||
if (!session)
|
if (!session)
|
||||||
session = bound_session ? bound_session : &legacy_io_session;
|
session = bound_session ? bound_session : &legacy_io_session;
|
||||||
@@ -257,10 +270,11 @@ bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t dat
|
|||||||
log_debug_message(LOG_DEBUG_IO, " Sending n Data: %zu", data_size);
|
log_debug_message(LOG_DEBUG_IO, " Sending n Data: %zu", data_size);
|
||||||
if (!session)
|
if (!session)
|
||||||
return false;
|
return false;
|
||||||
|
int timeout_sec = session->io_timeout_sec > 0 ? session->io_timeout_sec : SEND_TIMEOUT_SEC;
|
||||||
int fd = session->write_fd;
|
int fd = session->write_fd;
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
deadline.tv_sec += SEND_TIMEOUT_SEC;
|
deadline.tv_sec += timeout_sec;
|
||||||
short wait_events = POLLOUT;
|
short wait_events = POLLOUT;
|
||||||
ssize_t total_bytes_send = 0;
|
ssize_t total_bytes_send = 0;
|
||||||
while ((size_t)total_bytes_send < data_size) {
|
while ((size_t)total_bytes_send < data_size) {
|
||||||
@@ -306,7 +320,10 @@ bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t
|
|||||||
int timeout_sec);
|
int timeout_sec);
|
||||||
|
|
||||||
bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size) {
|
bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size) {
|
||||||
return protocol_receive_n_data_timed(session, data, data_size, RECEIVE_TIMEOUT_SEC);
|
/* Honor the session's configured deadline; protocol_receive_n_data_timed
|
||||||
|
* re-applies the built-in 60 s default when the value is <= 0. */
|
||||||
|
int timeout_sec = session ? session->io_timeout_sec : 0;
|
||||||
|
return protocol_receive_n_data_timed(session, data, data_size, timeout_sec);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t data_size,
|
bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t data_size,
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ typedef struct ProtocolSession {
|
|||||||
atomic_ullong total_allocated_bytes;
|
atomic_ullong total_allocated_bytes;
|
||||||
bool eight_bit_output;
|
bool eight_bit_output;
|
||||||
unsigned long long max_alloc;
|
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;
|
} ProtocolSession;
|
||||||
|
|
||||||
typedef int Status;
|
typedef int Status;
|
||||||
@@ -141,6 +147,15 @@ void protocol_session_unbind(void);
|
|||||||
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl);
|
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_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec);
|
||||||
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc);
|
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_alloc(size_t size);
|
||||||
void* protocol_realloc(void* ptr, size_t size);
|
void* protocol_realloc(void* ptr, size_t size);
|
||||||
void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled);
|
void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "test_property.h"
|
#include "test_property.h"
|
||||||
#include "test_protocol.h"
|
#include "test_protocol.h"
|
||||||
#include "test_queue.h"
|
#include "test_queue.h"
|
||||||
|
#include "test_receiver_timeout.h"
|
||||||
#include "test_robustness.h"
|
#include "test_robustness.h"
|
||||||
#include "test_scanner.h"
|
#include "test_scanner.h"
|
||||||
#include "test_server.h"
|
#include "test_server.h"
|
||||||
@@ -61,6 +62,7 @@ int main() {
|
|||||||
RUN_TEST(test_delta);
|
RUN_TEST(test_delta);
|
||||||
RUN_TEST(test_data);
|
RUN_TEST(test_data);
|
||||||
RUN_TEST(test_protocol);
|
RUN_TEST(test_protocol);
|
||||||
|
RUN_TEST(test_receiver_timeout);
|
||||||
RUN_TEST(test_metadata);
|
RUN_TEST(test_metadata);
|
||||||
RUN_TEST(test_glob);
|
RUN_TEST(test_glob);
|
||||||
RUN_TEST(test_iconv);
|
RUN_TEST(test_iconv);
|
||||||
|
|||||||
@@ -412,6 +412,35 @@ static void test_protocol_accounting_release_does_not_underflow() {
|
|||||||
protocol_session_unbind();
|
protocol_session_unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_protocol_session_io_timeout() {
|
||||||
|
/* Default is the built-in 60 s window; the setter stores exactly what it is
|
||||||
|
* given (<= 0 means "fall back to the default") so callers can propagate
|
||||||
|
* --timeout without special-casing 0. */
|
||||||
|
ProtocolSession session;
|
||||||
|
protocol_session_init(&session, -1, -1);
|
||||||
|
EXPECT_EQ_INT(session.io_timeout_sec, 60);
|
||||||
|
|
||||||
|
protocol_session_set_io_timeout(&session, 120);
|
||||||
|
EXPECT_EQ_INT(session.io_timeout_sec, 120);
|
||||||
|
protocol_session_set_io_timeout(&session, 0);
|
||||||
|
EXPECT_EQ_INT(session.io_timeout_sec, 0);
|
||||||
|
/* A NULL session is a no-op, not a crash. */
|
||||||
|
protocol_session_set_io_timeout(NULL, 5);
|
||||||
|
|
||||||
|
/* A short per-session deadline must actually bound a non-responsive read:
|
||||||
|
* with no writer the poll waits for the configured 1 s and then fails,
|
||||||
|
* rather than the built-in 60 s. */
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
|
ProtocolSession timed;
|
||||||
|
protocol_session_init(&timed, p[0], p[1]);
|
||||||
|
protocol_session_set_io_timeout(&timed, 1);
|
||||||
|
char buf[4];
|
||||||
|
EXPECT_FALSE(protocol_receive_n_data(&timed, buf, sizeof(buf)));
|
||||||
|
close(p[0]);
|
||||||
|
close(p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_send_receive_status_timed() {
|
static void test_send_receive_status_timed() {
|
||||||
int p[2];
|
int p[2];
|
||||||
EXPECT_EQ_INT(pipe(p), 0);
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
@@ -440,6 +469,7 @@ void test_protocol() {
|
|||||||
test_send_receive_data();
|
test_send_receive_data();
|
||||||
test_send_receive_int();
|
test_send_receive_int();
|
||||||
test_send_receive_status();
|
test_send_receive_status();
|
||||||
|
test_protocol_session_io_timeout();
|
||||||
test_send_receive_status_timed();
|
test_send_receive_status_timed();
|
||||||
test_receive_n_data_truncated();
|
test_receive_n_data_truncated();
|
||||||
test_receive_str_truncated();
|
test_receive_str_truncated();
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "test_receiver_timeout.h"
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "receiver.h"
|
||||||
|
#include "test_utils.h"
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static bool sink_discard(File* file, void* context) {
|
||||||
|
(void)context;
|
||||||
|
file_destroy(file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The idle/session bound is a pure function of three monotonic timestamps, so
|
||||||
|
* it can be exercised deterministically without sleeping an hour. A tiny
|
||||||
|
* overridden limit covers the same arithmetic the loop uses. */
|
||||||
|
static void test_receiver_time_limit_predicate() {
|
||||||
|
receiver_set_time_limits(10, 100);
|
||||||
|
struct timespec start = {.tv_sec = 1000, .tv_nsec = 0};
|
||||||
|
struct timespec fresh = {.tv_sec = 1000, .tv_nsec = 0};
|
||||||
|
struct timespec just_idle = {.tv_sec = 1009, .tv_nsec = 0}; /* 9 s no progress */
|
||||||
|
struct timespec idle = {.tv_sec = 1010, .tv_nsec = 0}; /* 10 s no progress */
|
||||||
|
struct timespec just_wall = {.tv_sec = 1099, .tv_nsec = 0};
|
||||||
|
struct timespec wall = {.tv_sec = 1100, .tv_nsec = 0}; /* 100 s session */
|
||||||
|
struct timespec wp_just = {.tv_sec = 1098, .tv_nsec = 0}; /* idle 1 s */
|
||||||
|
struct timespec wp_wall = {.tv_sec = 1099, .tv_nsec = 0}; /* idle 1 s */
|
||||||
|
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &fresh, &fresh));
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &fresh, &just_idle));
|
||||||
|
EXPECT_TRUE(receiver_time_limit_exceeded(&start, &fresh, &idle));
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &wp_just, &just_wall));
|
||||||
|
EXPECT_TRUE(receiver_time_limit_exceeded(&start, &wp_wall, &wall));
|
||||||
|
|
||||||
|
/* Reset restores the generous production defaults (1 h idle / 24 h total). */
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
struct timespec under_hour = {.tv_sec = 1000 + 3599, .tv_nsec = 0};
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &start, &under_hour));
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drive the actual receive loop with a test-only idle limit of 0 so the very
|
||||||
|
* first keepalive is rejected: this exercises the loop's abort path (log +
|
||||||
|
* STATUS_ERROR + return -1) with no timing dependence. */
|
||||||
|
static void test_receiver_aborts_idle_keepalive() {
|
||||||
|
receiver_set_time_limits(0, 3600);
|
||||||
|
int sv[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
|
||||||
|
Config* config = config_create();
|
||||||
|
EXPECT_NOT_NULL(config);
|
||||||
|
ReceiverSink sink = {.store_file = sink_discard,
|
||||||
|
.context = NULL,
|
||||||
|
.send_error = true,
|
||||||
|
.send_success = false,
|
||||||
|
.send_success_frame = NULL};
|
||||||
|
|
||||||
|
/* Bind an explicit session so the fd-based receive helpers use the
|
||||||
|
* socketpair rather than any transport left over from an earlier test. */
|
||||||
|
ProtocolSession session;
|
||||||
|
protocol_session_init(&session, sv[1], sv[1]);
|
||||||
|
protocol_session_bind(&session);
|
||||||
|
|
||||||
|
Status keepalive = STATUS_KEEPALIVE;
|
||||||
|
ssize_t wrote = write(sv[0], &keepalive, sizeof(keepalive));
|
||||||
|
int result = -2;
|
||||||
|
if (wrote == (ssize_t)sizeof(keepalive))
|
||||||
|
result = receiver_process_pending(config, sv[1], &sink, NULL);
|
||||||
|
Status reply = STATUS_OK;
|
||||||
|
ssize_t got = -1;
|
||||||
|
if (result == -1)
|
||||||
|
got = read(sv[0], &reply, sizeof(reply));
|
||||||
|
|
||||||
|
/* Tear down the binding/descriptors BEFORE asserting: an EXPECT_* failure
|
||||||
|
* returns immediately, and a dangling bound_session would poison later
|
||||||
|
* fd-level protocol I/O tests. */
|
||||||
|
protocol_session_unbind();
|
||||||
|
config_delete(config);
|
||||||
|
close(sv[0]);
|
||||||
|
close(sv[1]);
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
|
||||||
|
EXPECT_EQ_INT((int)wrote, (int)sizeof(keepalive));
|
||||||
|
EXPECT_EQ_INT(result, -1);
|
||||||
|
EXPECT_EQ_INT((int)got, (int)sizeof(reply));
|
||||||
|
EXPECT_EQ_INT((int)reply, (int)STATUS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_receiver_timeout(void) {
|
||||||
|
/* EXPECT_* returns from the current function on failure, so reset the
|
||||||
|
* process-global limits around the subtests (and again after) to guarantee a
|
||||||
|
* failed assertion cannot leave the receiver aborted for later tests. */
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
test_receiver_time_limit_predicate();
|
||||||
|
test_receiver_aborts_idle_keepalive();
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef TEST_RECEIVER_TIMEOUT_H
|
||||||
|
#define TEST_RECEIVER_TIMEOUT_H
|
||||||
|
|
||||||
|
void test_receiver_timeout(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user