diff --git a/README.md b/README.md index 2d59b66..34059c2 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ partial, alternate, and planned behavior. | `--existing` | Skip files not already present at the destination; update existing files normally. | | `--bwlimit ` | Bandwidth limit in kilobytes per second | | `--chunk-size ` | Chunk size in bytes (default: 10485760) | -| `--timeout ` | I/O timeout in seconds (default: 30) | +| `--timeout ` | 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 ` | Connection timeout in seconds (default: 10) | | `--backup` | Backup existing destination files before overwriting | | `--backup-dir ` | Target directory for backups (requires `--backup`) | @@ -151,6 +151,19 @@ partial, alternate, and planned behavior. | `--ca ` | TLS CA certificate file for verification (PEM) | | `--client-cn ` | 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 | Argument | Description | @@ -384,7 +397,7 @@ features without changing the meaning of ordinary compatibility options. | `--bwlimit ` | Apply token-bucket bandwidth limiting. | | `--progress` | Show transfer progress and throughput. | | `--stats` | Print transfer statistics. | -| `--timeout ` | Set I/O timeout. | +| `--timeout ` | 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 ` | Set connection timeout. | Short-option conflicts with rsync have been resolved for the CLI namespace diff --git a/src/client/client_send.c b/src/client/client_send.c index 16ed906..a0bb9dc 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -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; diff --git a/src/server/receiver.c b/src/server/receiver.c index 4b84831..a0a3071 100644 --- a/src/server/receiver.c +++ b/src/server/receiver.c @@ -12,6 +12,7 @@ #include "utils.h" #include #include +#include bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) { if (!outcomes) @@ -153,6 +154,93 @@ 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; +} + +/* 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) { 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; 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, sink)) + 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 +368,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, sink)) + goto fail; } if (status != STATUS_FINISHED) { log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status"); diff --git a/src/server/receiver.h b/src/server/receiver.h index 1b07e13..9f3efa3 100644 --- a/src/server/receiver.h +++ b/src/server/receiver.h @@ -4,6 +4,8 @@ #include "config.h" #include "file.h" #include "file_receive.h" +#include +#include 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 diff --git a/src/server/server.c b/src/server/server.c index 92b65cf..da3e65d 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -609,6 +609,12 @@ 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); + /* 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) { log_message(LOG_LEVEL_ERROR, "No server-side destination root configured"); goto done; @@ -743,6 +749,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); diff --git a/src/shared/config.c b/src/shared/config.c index 4816e06..6730639 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -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; diff --git a/src/shared/config.h b/src/shared/config.h index d582be2..77febe8 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -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; diff --git a/src/shared/file_send.c b/src/shared/file_send.c index e7bcffb..f15b77a 100644 --- a/src/shared/file_send.c +++ b/src/shared/file_send.c @@ -145,7 +145,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta off_t offset = 0; struct timespec 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) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); diff --git a/src/shared/protocol.c b/src/shared/protocol.c index 3f13667..3c3eee0 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -76,10 +76,23 @@ 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; +} + +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) { if (!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); 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 +320,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, diff --git a/src/shared/protocol.h b/src/shared/protocol.h index e23a68f..60f6dec 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -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,15 @@ 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); diff --git a/tests/runner.c b/tests/runner.c index 3837a6f..ec58de9 100644 --- a/tests/runner.c +++ b/tests/runner.c @@ -23,6 +23,7 @@ #include "test_property.h" #include "test_protocol.h" #include "test_queue.h" +#include "test_receiver_timeout.h" #include "test_robustness.h" #include "test_scanner.h" #include "test_server.h" @@ -61,6 +62,7 @@ int main() { RUN_TEST(test_delta); RUN_TEST(test_data); RUN_TEST(test_protocol); + RUN_TEST(test_receiver_timeout); RUN_TEST(test_metadata); RUN_TEST(test_glob); RUN_TEST(test_iconv); diff --git a/tests/test_protocol.c b/tests/test_protocol.c index 79fb8a0..273861e 100644 --- a/tests/test_protocol.c +++ b/tests/test_protocol.c @@ -412,6 +412,35 @@ static void test_protocol_accounting_release_does_not_underflow() { 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() { int p[2]; EXPECT_EQ_INT(pipe(p), 0); @@ -440,6 +469,7 @@ void test_protocol() { test_send_receive_data(); test_send_receive_int(); test_send_receive_status(); + test_protocol_session_io_timeout(); test_send_receive_status_timed(); test_receive_n_data_truncated(); test_receive_str_truncated(); diff --git a/tests/test_receiver_timeout.c b/tests/test_receiver_timeout.c new file mode 100644 index 0000000..8066b5c --- /dev/null +++ b/tests/test_receiver_timeout.c @@ -0,0 +1,97 @@ +#include "test_receiver_timeout.h" + +#include "protocol.h" +#include "receiver.h" +#include "test_utils.h" +#include +#include +#include + +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(); +} diff --git a/tests/test_receiver_timeout.h b/tests/test_receiver_timeout.h new file mode 100644 index 0000000..c320aec --- /dev/null +++ b/tests/test_receiver_timeout.h @@ -0,0 +1,6 @@ +#ifndef TEST_RECEIVER_TIMEOUT_H +#define TEST_RECEIVER_TIMEOUT_H + +void test_receiver_timeout(void); + +#endif