Release v2.26.0 #284
@@ -190,6 +190,26 @@ static bool store_max_connections(int* slot, const char* value, const char* modu
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Parse a non-negative concurrency cap where 0 means unlimited/disabled
|
||||
* (per-module `max connections`, `max connections per host`,
|
||||
* `auth lockout threshold`). Negative/garbage/oversized values are rejected. */
|
||||
static bool store_optional_cap(int* slot, const char* value, int max_value, const char* key,
|
||||
const char* module_name, char* err, size_t err_size) {
|
||||
char* end = NULL;
|
||||
errno = 0;
|
||||
long n = strtol(value, &end, 10);
|
||||
if (*value == '\0' || errno != 0 || *end != '\0' || n < 0 || n > max_value) {
|
||||
if (module_name)
|
||||
set_error(err, err_size, "module '%s': invalid '%s' '%s' (must be 0-%d)", module_name, key,
|
||||
value, max_value);
|
||||
else
|
||||
set_error(err, err_size, "invalid '%s' '%s' (must be 0-%d)", key, value, max_value);
|
||||
return false;
|
||||
}
|
||||
*slot = (int)n;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Parse an `auth failure delay` value: 0 (disabled) through the configured cap. */
|
||||
static bool store_auth_failure_delay(int* slot, const char* value, char* err, size_t err_size) {
|
||||
char* end = NULL;
|
||||
@@ -227,6 +247,9 @@ DaemonConf* daemon_conf_create(void) {
|
||||
conf->global.port = DAEMON_CONF_DEFAULT_PORT;
|
||||
conf->global.max_connections = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS;
|
||||
conf->global.auth_failure_delay_ms = DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS;
|
||||
conf->global.max_connections_per_host = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS_PER_HOST;
|
||||
conf->global.auth_lockout_threshold = DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_THRESHOLD;
|
||||
conf->global.auth_lockout_duration_sec = DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_DURATION_SEC;
|
||||
return conf;
|
||||
}
|
||||
|
||||
@@ -312,8 +335,20 @@ static bool apply_global_key(DaemonConf* conf, char* key, const char* value, boo
|
||||
}
|
||||
if (key_equals(key, "max connections"))
|
||||
return store_max_connections(&conf->global.max_connections, value, NULL, err, err_size);
|
||||
if (key_equals(key, "max connections per host"))
|
||||
return store_optional_cap(&conf->global.max_connections_per_host, value,
|
||||
DAEMON_CONF_MAX_CONCURRENCY_LIMIT, "max connections per host", NULL,
|
||||
err, err_size);
|
||||
if (key_equals(key, "auth failure delay"))
|
||||
return store_auth_failure_delay(&conf->global.auth_failure_delay_ms, value, err, err_size);
|
||||
if (key_equals(key, "auth lockout threshold"))
|
||||
return store_optional_cap(&conf->global.auth_lockout_threshold, value,
|
||||
DAEMON_CONF_MAX_CONCURRENCY_LIMIT, "auth lockout threshold", NULL,
|
||||
err, err_size);
|
||||
if (key_equals(key, "auth lockout duration"))
|
||||
return store_optional_cap(&conf->global.auth_lockout_duration_sec, value,
|
||||
DAEMON_CONF_MAX_AUTH_LOCKOUT_DURATION_SEC, "auth lockout duration",
|
||||
NULL, err, err_size);
|
||||
if (key_equals(key, "hosts allow"))
|
||||
return store_host_list(&conf->global.hosts_allow, &conf->global.hosts_allow_count, value,
|
||||
"hosts allow", NULL, replace_hosts, err, err_size);
|
||||
@@ -400,7 +435,8 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
||||
return true;
|
||||
}
|
||||
if (key_equals(key, "max connections"))
|
||||
return store_max_connections(&module->max_connections, value, module->name, err, err_size);
|
||||
return store_optional_cap(&module->max_connections, value, DAEMON_CONF_MAX_CONCURRENCY_LIMIT,
|
||||
"max connections", module->name, err, err_size);
|
||||
if (key_equals(key, "hosts allow"))
|
||||
return store_host_list(&module->hosts_allow, &module->hosts_allow_count, value, "hosts allow",
|
||||
false, module->name, err, err_size);
|
||||
@@ -444,6 +480,11 @@ static int open_module(DaemonConf* conf, int* current_module, const char* name,
|
||||
set_error(err, err_size, "duplicate module '%s'", name);
|
||||
return -1;
|
||||
}
|
||||
if (conf->module_count >= DAEMON_CONF_MAX_MODULES) {
|
||||
set_error(err, err_size, "too many modules (limit %d); module '%s' rejected",
|
||||
DAEMON_CONF_MAX_MODULES, name);
|
||||
return -1;
|
||||
}
|
||||
DaemonModule* grown =
|
||||
realloc(conf->modules, (size_t)(conf->module_count + 1) * sizeof(DaemonModule));
|
||||
if (!grown) {
|
||||
|
||||
+41
-15
@@ -52,11 +52,10 @@ typedef struct DaemonModule {
|
||||
activities. Without it the daemon refuses all of them. */
|
||||
char** auth_users; /* `auth users = a,b`; Wave B credential list */
|
||||
int auth_user_count;
|
||||
/* `max connections = N` (optional per-module cap). 0 means "not set"
|
||||
* (inherit the global cap). Parsed, stored, and validated, but NOT enforced
|
||||
* per-module: connections are counted in the accept-loop parent before the
|
||||
* client's module is known, so only the global cap is enforced (see
|
||||
* transport_tcp.c and the Daemon Mode notes in RSYNC_COMPAT.md). */
|
||||
/* `max connections = N` (optional per-module cap). 0 means unlimited. The
|
||||
* per-connection child records the selected module in the shared registry
|
||||
* (daemon_limits.c) once the config frame names it, so the cap is enforced
|
||||
* across all forked children; the parent reclaims the slot on SIGCHLD. */
|
||||
int max_connections;
|
||||
char** hosts_allow; /* `hosts allow = a,b`; host access allow patterns */
|
||||
int hosts_allow_count;
|
||||
@@ -67,14 +66,25 @@ typedef struct DaemonModule {
|
||||
/* Global (pre-module) scalar keys. `motd file` is parsed and stored but has
|
||||
* no wire effect yet (MOTD display is Wave C). */
|
||||
typedef struct DaemonConfGlobals {
|
||||
int port; /* `port`, default DAEMON_CONF_DEFAULT_PORT (873) */
|
||||
char* motd_file; /* `motd file`, may be NULL */
|
||||
char* address; /* `address` (optional bind address), may be NULL */
|
||||
int max_connections; /* `max connections`, default
|
||||
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */
|
||||
int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default
|
||||
DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS */
|
||||
char** hosts_allow; /* `hosts allow`; global host access allow patterns */
|
||||
int port; /* `port`, default DAEMON_CONF_DEFAULT_PORT (873) */
|
||||
char* motd_file; /* `motd file`, may be NULL */
|
||||
char* address; /* `address` (optional bind address), may be NULL */
|
||||
int max_connections; /* `max connections`, default
|
||||
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */
|
||||
int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default
|
||||
DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS */
|
||||
int max_connections_per_host; /* `max connections per host`, concurrent cap per
|
||||
source IP; default
|
||||
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS_PER_HOST (0 =
|
||||
unlimited) */
|
||||
int auth_lockout_threshold; /* `auth lockout threshold`, failed attempts from
|
||||
one source before lockout; default
|
||||
DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_THRESHOLD (0
|
||||
disables) */
|
||||
int auth_lockout_duration_sec; /* `auth lockout duration`, seconds; default
|
||||
DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_DURATION_SEC
|
||||
(0 disables) */
|
||||
char** hosts_allow; /* `hosts allow`; global host access allow patterns */
|
||||
int hosts_allow_count;
|
||||
char** hosts_deny; /* `hosts deny`; global host access deny patterns */
|
||||
int hosts_deny_count;
|
||||
@@ -92,11 +102,26 @@ typedef struct DaemonConf {
|
||||
#define DAEMON_CONF_DEFAULT_MAX_CONNECTIONS 100
|
||||
/* Default `auth failure delay` in milliseconds (0 disables the throttle). */
|
||||
#define DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS 500
|
||||
/* Default `max connections per host` (0 = unlimited). */
|
||||
#define DAEMON_CONF_DEFAULT_MAX_CONNECTIONS_PER_HOST 0
|
||||
/* Default cross-process auth lockout: 10 failed attempts from one source lock
|
||||
* it out for 300 s (0 disables either knob). */
|
||||
#define DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_THRESHOLD 10
|
||||
#define DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_DURATION_SEC 300
|
||||
/* Upper bound on a `max connections per host` or `auth lockout threshold`
|
||||
* value, so a typo cannot size the shared registry absurdly. */
|
||||
#define DAEMON_CONF_MAX_CONCURRENCY_LIMIT 1000000
|
||||
/* Upper bound on `auth lockout duration` (7 days). */
|
||||
#define DAEMON_CONF_MAX_AUTH_LOCKOUT_DURATION_SEC 604800
|
||||
/* Largest accepted `auth failure delay`, so a typo cannot pin a connection
|
||||
* child in nanosleep for an absurd time. */
|
||||
/* Bounded well below the socket I/O timeout so a failed-auth child cannot hold
|
||||
* a connection slot for long enough to amplify connection-cap exhaustion. */
|
||||
#define DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS 5000
|
||||
/* Upper bound on the number of [module] sections, so the shared registry's
|
||||
* per-module counter array stays fixed-size. The parser rejects the next
|
||||
* section past this bound. */
|
||||
#define DAEMON_CONF_MAX_MODULES 256
|
||||
/* Longest accepted config line (excluding the trailing newline). Longer lines
|
||||
* are rejected rather than buffered unboundedly. */
|
||||
#define DAEMON_CONF_MAX_LINE 4096
|
||||
@@ -129,8 +154,9 @@ bool daemon_module_name_valid(const char* name);
|
||||
/* Parse one --dparam=KEY=VALUE (or "--dparam KEY=VALUE") override string and
|
||||
* apply it to the global keys only. Keys are case-insensitive and limited to
|
||||
* the global keys defined by the grammar (port, motd file, address,
|
||||
* max connections, auth failure delay, hosts allow, hosts deny). Returns 0 on
|
||||
* success, -1 on error (err filled). */
|
||||
* max connections, max connections per host, auth failure delay,
|
||||
* auth lockout threshold, auth lockout duration, hosts allow, hosts deny).
|
||||
* Returns 0 on success, -1 on error (err filled). */
|
||||
int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err, size_t err_size);
|
||||
|
||||
/* Host access-control matching (pure; no I/O). `daemon_host_pattern_match`
|
||||
|
||||
@@ -33,6 +33,11 @@ static void test_daemon_conf_create_defaults() {
|
||||
EXPECT_NULL(conf->global.address);
|
||||
EXPECT_EQ_INT(conf->global.max_connections, DAEMON_CONF_DEFAULT_MAX_CONNECTIONS);
|
||||
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS);
|
||||
EXPECT_EQ_INT(conf->global.max_connections_per_host,
|
||||
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS_PER_HOST);
|
||||
EXPECT_EQ_INT(conf->global.auth_lockout_threshold, DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_THRESHOLD);
|
||||
EXPECT_EQ_INT(conf->global.auth_lockout_duration_sec,
|
||||
DAEMON_CONF_DEFAULT_AUTH_LOCKOUT_DURATION_SEC);
|
||||
EXPECT_EQ_INT(conf->global.hosts_allow_count, 0);
|
||||
EXPECT_EQ_INT(conf->global.hosts_deny_count, 0);
|
||||
EXPECT_EQ_INT(conf->module_count, 0);
|
||||
@@ -316,6 +321,12 @@ static void test_daemon_conf_dparam_override() {
|
||||
|
||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "max connections=7", err, sizeof(err)), 0);
|
||||
EXPECT_EQ_INT(conf->global.max_connections, 7);
|
||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "max connections per host=3", err, sizeof(err)), 0);
|
||||
EXPECT_EQ_INT(conf->global.max_connections_per_host, 3);
|
||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "auth lockout threshold=5", err, sizeof(err)), 0);
|
||||
EXPECT_EQ_INT(conf->global.auth_lockout_threshold, 5);
|
||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "auth lockout duration=120", err, sizeof(err)), 0);
|
||||
EXPECT_EQ_INT(conf->global.auth_lockout_duration_sec, 120);
|
||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "AUTH FAILURE DELAY=1500", err, sizeof(err)), 0);
|
||||
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, 1500);
|
||||
EXPECT_EQ_INT(
|
||||
@@ -390,6 +401,9 @@ static void test_daemon_conf_limits_and_hosts_parse() {
|
||||
char err[256];
|
||||
EXPECT_EQ_INT(write_conf("max connections = 25\n"
|
||||
"auth failure delay = 0\n"
|
||||
"max connections per host = 4\n"
|
||||
"auth lockout threshold = 3\n"
|
||||
"auth lockout duration = 60\n"
|
||||
"hosts allow = 10.0.0.0/8, 192.168.1.0/24\n"
|
||||
"hosts deny = 192.168.0.1 2001:db8::/32\n"
|
||||
"\n"
|
||||
@@ -405,6 +419,9 @@ static void test_daemon_conf_limits_and_hosts_parse() {
|
||||
EXPECT_NOT_NULL(conf);
|
||||
EXPECT_EQ_INT(conf->global.max_connections, 25);
|
||||
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, 0);
|
||||
EXPECT_EQ_INT(conf->global.max_connections_per_host, 4);
|
||||
EXPECT_EQ_INT(conf->global.auth_lockout_threshold, 3);
|
||||
EXPECT_EQ_INT(conf->global.auth_lockout_duration_sec, 60);
|
||||
EXPECT_EQ_INT(conf->global.hosts_allow_count, 2);
|
||||
EXPECT_EQ_STR(conf->global.hosts_allow[0], "10.0.0.0/8");
|
||||
EXPECT_EQ_STR(conf->global.hosts_allow[1], "192.168.1.0/24");
|
||||
@@ -419,11 +436,14 @@ static void test_daemon_conf_limits_and_hosts_parse() {
|
||||
daemon_conf_free(conf);
|
||||
|
||||
const char* bad_values[] = {
|
||||
"max connections = 0\n", "max connections = -1\n",
|
||||
"max connections = abc\n", "auth failure delay = -1\n",
|
||||
"auth failure delay = 70000\n", "auth failure delay = soon\n",
|
||||
"hosts allow = 10.0.0.0/99\n", "hosts deny = 2001:db8::/129\n",
|
||||
"hosts allow = *.example.com\n", "hosts deny = not-an-ip\n",
|
||||
"max connections = 0\n", "max connections = -1\n",
|
||||
"max connections = abc\n", "auth failure delay = -1\n",
|
||||
"auth failure delay = 70000\n", "auth failure delay = soon\n",
|
||||
"max connections per host = -1\n", "max connections per host = lots\n",
|
||||
"auth lockout threshold = -2\n", "auth lockout threshold = many\n",
|
||||
"auth lockout duration = -1\n", "auth lockout duration = forever\n",
|
||||
"hosts allow = 10.0.0.0/99\n", "hosts deny = 2001:db8::/129\n",
|
||||
"hosts allow = *.example.com\n", "hosts deny = not-an-ip\n",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(bad_values) / sizeof(bad_values[0]); i++) {
|
||||
EXPECT_EQ_INT(write_conf(bad_values[i], &path), 0);
|
||||
@@ -434,7 +454,8 @@ static void test_daemon_conf_limits_and_hosts_parse() {
|
||||
|
||||
/* The same strictness applies inside a module section. */
|
||||
const char* bad_module[] = {
|
||||
"[m]\npath = /x\nmax connections = 0\n",
|
||||
"[m]\npath = /x\nmax connections = -1\n",
|
||||
"[m]\npath = /x\nmax connections = abc\n",
|
||||
"[m]\npath = /x\nhosts allow = 10.0.0.0/40\n",
|
||||
"[m]\npath = /x\nhosts deny = 999.1.1.1/8\n",
|
||||
};
|
||||
@@ -446,6 +467,14 @@ static void test_daemon_conf_limits_and_hosts_parse() {
|
||||
EXPECT_TRUE(strstr(err, "invalid") != NULL);
|
||||
}
|
||||
|
||||
/* Module `max connections = 0` is now valid and means unlimited. */
|
||||
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nmax connections = 0\n", &path), 0);
|
||||
conf = daemon_conf_load(path, err, sizeof(err));
|
||||
free(path);
|
||||
EXPECT_NOT_NULL(conf);
|
||||
EXPECT_EQ_INT(conf->modules[0].max_connections, 0);
|
||||
daemon_conf_free(conf);
|
||||
|
||||
/* An empty hosts list is not an error (no patterns are added). */
|
||||
EXPECT_EQ_INT(write_conf("hosts allow = \n[m]\npath = /x\n", &path), 0);
|
||||
conf = daemon_conf_load(path, err, sizeof(err));
|
||||
@@ -509,6 +538,27 @@ static void test_daemon_module_name_valid() {
|
||||
}
|
||||
}
|
||||
|
||||
static void test_daemon_conf_module_count_capped() {
|
||||
size_t cap = DAEMON_CONF_MAX_MODULES;
|
||||
size_t len = (cap + 8) * 32;
|
||||
char* body = malloc(len);
|
||||
EXPECT_NOT_NULL(body);
|
||||
body[0] = '\0';
|
||||
for (size_t i = 0; i < cap + 1; i++) {
|
||||
char line[48];
|
||||
snprintf(line, sizeof(line), "[m%zu]\npath = /x\n", i);
|
||||
strcat(body, line);
|
||||
}
|
||||
char* path;
|
||||
EXPECT_EQ_INT(write_conf(body, &path), 0);
|
||||
free(body);
|
||||
char err[256];
|
||||
const DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
|
||||
free(path);
|
||||
EXPECT_NULL(conf);
|
||||
EXPECT_TRUE(strstr(err, "too many modules") != NULL);
|
||||
}
|
||||
|
||||
void test_daemon_conf() {
|
||||
test_daemon_conf_create_defaults();
|
||||
test_daemon_conf_full_parse();
|
||||
@@ -525,6 +575,7 @@ void test_daemon_conf() {
|
||||
test_daemon_conf_dparam_override();
|
||||
test_daemon_conf_auth_users_validated();
|
||||
test_daemon_conf_limits_and_hosts_parse();
|
||||
test_daemon_conf_module_count_capped();
|
||||
test_daemon_hosts_allowed();
|
||||
test_daemon_module_name_valid();
|
||||
}
|
||||
Reference in New Issue
Block a user