fix(protocol): honor --timeout for protocol I/O; bound idle/session time
This commit is contained in:
@@ -1435,6 +1435,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
}
|
||||
ProtocolSession session;
|
||||
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_bind(&session);
|
||||
if (!config_send(client->file_descriptor, context->config)) {
|
||||
@@ -1896,6 +1897,7 @@ int send_files(Config* config) {
|
||||
}
|
||||
ProtocolSession session;
|
||||
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_bind(&session);
|
||||
int ret = 1;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) {
|
||||
if (!outcomes)
|
||||
@@ -153,6 +154,74 @@ static bool receiver_process_batch(Config* config, int file_descriptor) {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Refresh the progress timestamp for a forward-moving frame and enforce the
|
||||
* bounds above. Returns false (after best-effort STATUS_ERROR) when the
|
||||
* connection must be dropped. */
|
||||
static bool receiver_note_status(const struct timespec* session_start,
|
||||
struct timespec* last_progress, Status status,
|
||||
int file_descriptor) {
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
if (status != STATUS_KEEPALIVE && status != STATUS_ABORT)
|
||||
*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);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
|
||||
return receiver_process_pending(config, file_descriptor, sink, NULL);
|
||||
}
|
||||
@@ -172,6 +241,15 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
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))
|
||||
return -1;
|
||||
bool early_delete = config_delete_timing_early(config);
|
||||
/* 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
|
||||
@@ -271,6 +349,8 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
next_status:
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
goto receive_error;
|
||||
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor))
|
||||
goto fail;
|
||||
}
|
||||
if (status != STATUS_FINISHED) {
|
||||
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "config.h"
|
||||
#include "file.h"
|
||||
#include "file_receive.h"
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
|
||||
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);
|
||||
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
|
||||
|
||||
@@ -609,6 +609,11 @@ void handler(int file_descriptor) {
|
||||
if (gate_ctx.super_mode_override != -1)
|
||||
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
||||
protocol_set_8_bit_output(config->eight_bit_output);
|
||||
/* Honor the negotiated --timeout for every protocol frame from here on (the
|
||||
* config handshake itself used the built-in 60 s window). A positive value
|
||||
* also tightens the socket SO_RCVTIMEO/SO_SNDTIMEO already applied by the
|
||||
* transport; 0 leaves both built-in defaults in place. */
|
||||
protocol_session_set_io_timeout(&session, config->timeout);
|
||||
if (!authorized_root) {
|
||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||
goto done;
|
||||
@@ -743,6 +748,7 @@ void handler(int file_descriptor) {
|
||||
goto done;
|
||||
}
|
||||
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_load(&session.total_allocated_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->server_host = str_dup("127.0.0.1");
|
||||
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->quiet = false;
|
||||
config->backup = false;
|
||||
|
||||
@@ -157,7 +157,12 @@ typedef struct Config {
|
||||
char* tls_cert;
|
||||
char* tls_key;
|
||||
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;
|
||||
/* --contimeout: connect()/accept timeout, transport layer only. */
|
||||
int contimeout;
|
||||
bool quiet;
|
||||
bool backup;
|
||||
|
||||
+13
-2
@@ -76,10 +76,17 @@ void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd)
|
||||
session->read_fd = read_fd;
|
||||
session->write_fd = write_fd;
|
||||
session->max_alloc = DEFAULT_MAX_ALLOC;
|
||||
session->io_timeout_sec = RECEIVE_TIMEOUT_SEC;
|
||||
atomic_init(&session->total_allocated_bytes, 0);
|
||||
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;
|
||||
}
|
||||
|
||||
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc) {
|
||||
if (!session)
|
||||
session = bound_session ? bound_session : &legacy_io_session;
|
||||
@@ -257,10 +264,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);
|
||||
if (!session)
|
||||
return false;
|
||||
int timeout_sec = session->io_timeout_sec > 0 ? session->io_timeout_sec : SEND_TIMEOUT_SEC;
|
||||
int fd = session->write_fd;
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||
deadline.tv_sec += SEND_TIMEOUT_SEC;
|
||||
deadline.tv_sec += timeout_sec;
|
||||
short wait_events = POLLOUT;
|
||||
ssize_t total_bytes_send = 0;
|
||||
while ((size_t)total_bytes_send < data_size) {
|
||||
@@ -306,7 +314,10 @@ bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t
|
||||
int timeout_sec);
|
||||
|
||||
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,
|
||||
|
||||
@@ -52,6 +52,12 @@ typedef struct ProtocolSession {
|
||||
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;
|
||||
@@ -141,6 +147,11 @@ 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);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user