fix(daemon): close ACL fail-opens (v4-mapped peers, invalid patterns) and cap auth delay

This commit is contained in:
2026-09-13 02:51:07 +02:00
parent dff6609976
commit fc560246c1
6 changed files with 66 additions and 34 deletions
+3 -2
View File
@@ -506,8 +506,9 @@ A `[module]` may also set `max connections` (parsed and validated but not
enforced per module — the global cap applies to the whole listener) and its own enforced per module — the global cap applies to the whole listener) and its own
`hosts allow`/`hosts deny`. `hosts allow`/`hosts deny`.
Host patterns are `*` (match all), IPv4/IPv6 literals, IPv4/IPv6 CIDR Host patterns are `*` (match all), IPv4/IPv6 literals, or IPv4/IPv6 CIDR
(`10.0.0.0/8`, `2001:db8::/32`), or hostname globs (`*.example.com`). A matching (`10.0.0.0/8`, `2001:db8::/32`). Hostnames are not resolved, so hostname globs
are rejected at parse time rather than silently never matching. A matching
`hosts deny` rejects; if any `hosts allow` entries exist, a peer matching none of `hosts deny` rejects; if any `hosts allow` entries exist, a peer matching none of
them is rejected; deny takes precedence over allow. The global list is checked them is rejected; deny takes precedence over allow. The global list is checked
before the module list, before authentication, and the connecting peer address before the module list, before authentication, and the connecting peer address
+3 -3
View File
@@ -635,9 +635,9 @@ now transmits targets (the prior behavior was broken/partial); its status moved
**Daemon Mode notes (Wave A protocol 2.15.0; A7 auth protocol 2.19.0; MOTD no bump):** FastSync daemon mode is supported in FastSync's own protocol/config grammar, not rsync's SMB/daemon option encoding. **Daemon Mode notes (Wave A protocol 2.15.0; A7 auth protocol 2.19.0; MOTD no bump):** FastSync daemon mode is supported in FastSync's own protocol/config grammar, not rsync's SMB/daemon option encoding.
- **Config grammar** (`fastsyncd.conf`): line-based; an implicit global section first, then `[module]` sections. Keys are case-insensitive, values are trimmed and may be wrapped in one layer of double quotes (`path = "/srv/my dir"`). `#` and `;` at the start of a line (after leading whitespace) are full-line comments; inline comments and `\` continuations are not supported. Lines are bounded (4096 chars). Global keys: `port` (default 873), `motd file` (the daemon sends its bounded, escaped content to a client after the module gate/auth accepts, unless the client passes `--no-motd`), `address` (optional bind address), `max connections` (positive integer cap on concurrent connections, default 100; 0/negative/garbage is a parse error), `auth failure delay` (milliseconds to sleep after a failed authentication, default 500; 0 disables, capped at 60000), `hosts allow` and `hosts deny` (comma- and/or whitespace-separated host access patterns — see the host access control note below). Module keys: `path` (required; the daemon-side authorized root for that module), `read only` (yes/no/true/false/1/0, default no), `client owner` (yes/no/true/false/1/0, default no; opts the module into client-chosen ownership — see below), `auth users` (comma list), `max connections` (optional per-module cap; parsed and stored but **not enforced** — the global cap applies to the whole listener), `hosts allow`/`hosts deny` (per-module host access lists). **Unknown keys and malformed lines are parse-and-reject errors** (never silently ignored), so a typo cannot change what a module serves. - **Config grammar** (`fastsyncd.conf`): line-based; an implicit global section first, then `[module]` sections. Keys are case-insensitive, values are trimmed and may be wrapped in one layer of double quotes (`path = "/srv/my dir"`). `#` and `;` at the start of a line (after leading whitespace) are full-line comments; inline comments and `\` continuations are not supported. Lines are bounded (4096 chars). Global keys: `port` (default 873), `motd file` (the daemon sends its bounded, escaped content to a client after the module gate/auth accepts, unless the client passes `--no-motd`), `address` (optional bind address), `max connections` (positive integer cap on concurrent connections, default 100; 0/negative/garbage is a parse error), `auth failure delay` (milliseconds to sleep after a failed authentication, default 500; 0 disables, capped at 5000), `hosts allow` and `hosts deny` (comma- and/or whitespace-separated host access patterns — see the host access control note below). Module keys: `path` (required; the daemon-side authorized root for that module), `read only` (yes/no/true/false/1/0, default no), `client owner` (yes/no/true/false/1/0, default no; opts the module into client-chosen ownership — see below), `auth users` (comma list), `max connections` (optional per-module cap; parsed and stored but **not enforced** — the global cap applies to the whole listener), `hosts allow`/`hosts deny` (per-module host access lists). **Unknown keys and malformed lines are parse-and-reject errors** (never silently ignored), so a typo cannot change what a module serves.
- **Host access control (`hosts allow`/`hosts deny`):** both keys accept a comma- and/or whitespace-separated list of patterns and may appear globally and/or per module (multiple lines append). Supported patterns are `*` (match all), an IPv4 or IPv6 literal (`10.0.0.1`, `2001:db8::1`), an IPv4/IPv6 CIDR (`10.0.0.0/8`, `2001:db8::/32`), and a simple glob (`*.example.com`; globs are matched case-insensitively against the peer string, so a numeric peer never matches a hostname glob). rsync-like semantics: a matching `hosts deny` rejects; if any `hosts allow` entries exist, a peer matching none of them is rejected; deny takes precedence over allow. The daemon enforces the global list first, then the selected module's list, **before authentication** in `server_module_gate`, with an audit log line naming the peer, the module and the outcome. The numeric peer address is obtained with `getpeername`+`inet_ntop` (`utils_fd_peer_ip`, handling both address families); when it cannot be obtained a module with any ACL fails closed (refused), while an ACL-free module continues and logs at debug. A malformed pattern (e.g. an out-of-range CIDR prefix) is a parse error at load time. - **Host access control (`hosts allow`/`hosts deny`):** both keys accept a comma- and/or whitespace-separated list of patterns and may appear globally and/or per module (multiple config-file lines append; a `--dparam` override replaces). Supported patterns are `*` (match all), an IPv4 or IPv6 literal (`10.0.0.1`, `2001:db8::1`), and an IPv4/IPv6 CIDR (`10.0.0.0/8`, `2001:db8::/32`). Hostname patterns are **not** supported: because the peer is always a numeric address and no reverse DNS is performed, a hostname/glob pattern would silently never match, so it is rejected at load time (fail-closed) instead of being accepted as a dead rule. An IPv4 peer on a dual-stack IPv6 listener is normalized from its `::ffff:a.b.c.d` form so IPv4 patterns match it. rsync-like semantics: a matching `hosts deny` rejects; if any `hosts allow` entries exist, a peer matching none of them is rejected; deny takes precedence over allow. The daemon enforces the global list first, then the selected module's list, **before authentication** in `server_module_gate`, with an audit log line naming the peer, the module and the outcome. The numeric peer address is obtained with `getpeername`+`inet_ntop` (`utils_fd_peer_ip`, handling both address families); when it cannot be obtained a module with any ACL fails closed (refused), while an ACL-free module continues and logs at debug. A malformed pattern (e.g. an out-of-range CIDR prefix) is a parse error at load time.
- **Connection cap and auth throttle:** the global `max connections` key (default 100) is plumbed into the listener (`transport_tcp.c`), which rejects a connection once the accept-loop parent's active-child count reaches it; the IPv4/IPv6 peer is logged for every accepted connection. The optional per-module `max connections` key is parsed and validated but **not enforced** (connections are counted in the parent before the client's module is known); the daemon logs a startup warning for any module that sets it. On a failed authentication the per-connection child sleeps the global `auth failure delay` (default 500 ms, 0 disables, capped at 60000) via `nanosleep` before the connection closes, rate-limiting online guessing without delaying a success. - **Connection cap and auth throttle:** the global `max connections` key (default 100) is plumbed into the listener (`transport_tcp.c`), which rejects a connection once the accept-loop parent's active-child count reaches it; the IPv4/IPv6 peer is logged for every accepted connection. The optional per-module `max connections` key is parsed and validated but **not enforced** (connections are counted in the parent before the client's module is known); the daemon logs a startup warning for any module that sets it. On a failed authentication the per-connection child sleeps the global `auth failure delay` (default 500 ms, 0 disables, capped at 5000) via `nanosleep` before the connection closes, rate-limiting online guessing without delaying a success.
- **Module selection & confinement:** the client requests a module with an rsync-style `host::module[/path]` destination. The module name crosses the wire as a trailing string on the config frame (bumping `PROTOCOL_VERSION` 2.14.0 → 2.15.0; the bump is required because the config-frame layout changed and the strict same-version handshake is what prevents a peer from desynchronizing on the new trailing field). The daemon looks the module up in ITS OWN config and uses the module's `path` as the authorized root through the exact same `configure_authorization` confinement the standalone server applies to `--destination-root` (`file_open_secure_parent`, `has_path_traversal`, `path_is_within`); the client never supplies the root, every client-chosen-ownership/super-user request is refused unless the module declares `client owner = yes` (the daemon's per-module opt-in, see below), and the operator `--no-super` veto forces super-user activities off for every daemon connection. The client's `/path` part is relative inside the module and is rejected if absolute or if it contains `..`. Unknown modules are refused before any data moves (the run fails cleanly at the config handshake). An absolute destination and a module request against a non-daemon server are also refused. - **Module selection & confinement:** the client requests a module with an rsync-style `host::module[/path]` destination. The module name crosses the wire as a trailing string on the config frame (bumping `PROTOCOL_VERSION` 2.14.0 → 2.15.0; the bump is required because the config-frame layout changed and the strict same-version handshake is what prevents a peer from desynchronizing on the new trailing field). The daemon looks the module up in ITS OWN config and uses the module's `path` as the authorized root through the exact same `configure_authorization` confinement the standalone server applies to `--destination-root` (`file_open_secure_parent`, `has_path_traversal`, `path_is_within`); the client never supplies the root, every client-chosen-ownership/super-user request is refused unless the module declares `client owner = yes` (the daemon's per-module opt-in, see below), and the operator `--no-super` veto forces super-user activities off for every daemon connection. The client's `/path` part is relative inside the module and is rejected if absolute or if it contains `..`. Unknown modules are refused before any data moves (the run fails cleanly at the config handshake). An absolute destination and a module request against a non-daemon server are also refused.
- **`client owner` (client-chosen-ownership opt-in):** by default a daemon module refuses every request that would let the client pick an owner or ask for super-user activities — `--numeric-ids`, `--chown`, `--usermap`/`--groupmap`, `--fake-super`, `--copy-as`, and an explicit `--super` — at the config handshake (before `STATUS_OK`), because a daemon has no per-module opt-in for client-chosen ownership and any anonymous client could otherwise force arbitrary owner ids inside the module root. `client owner = yes` opts a single module in, allowing those requests within that module's root (the standalone listener and the SSH `--stdio` server always honor them for their single operator-authorized root). Without the opt-in the daemon also forces super-user **device** activity off for that connection — char/block device-node creation (`--devices`) and `--write-devices` — even under the default `AUTO` mode, so a non-opted module can never be made to `mknod` or write a raw device; those entries are skipped (not refused) so an ordinary `-a` push still succeeds without device nodes. The opt-in does **not** lift the privilege requirement: `--copy-as` still needs a root receiver, and the operator `--no-super` veto still forces super-user activities off for every connection. The daemon logs a prominent startup warning for each `client owner = yes` module so the operator's deliberate choice is visible. - **`client owner` (client-chosen-ownership opt-in):** by default a daemon module refuses every request that would let the client pick an owner or ask for super-user activities — `--numeric-ids`, `--chown`, `--usermap`/`--groupmap`, `--fake-super`, `--copy-as`, and an explicit `--super` — at the config handshake (before `STATUS_OK`), because a daemon has no per-module opt-in for client-chosen ownership and any anonymous client could otherwise force arbitrary owner ids inside the module root. `client owner = yes` opts a single module in, allowing those requests within that module's root (the standalone listener and the SSH `--stdio` server always honor them for their single operator-authorized root). Without the opt-in the daemon also forces super-user **device** activity off for that connection — char/block device-node creation (`--devices`) and `--write-devices` — even under the default `AUTO` mode, so a non-opted module can never be made to `mknod` or write a raw device; those entries are skipped (not refused) so an ordinary `-a` push still succeeds without device nodes. The opt-in does **not** lift the privilege requirement: `--copy-as` still needs a root receiver, and the operator `--no-super` veto still forces super-user activities off for every connection. The daemon logs a prominent startup warning for each `client owner = yes` module so the operator's deliberate choice is visible.
- **`read only` safe default:** every network transfer FastSync currently supports is a push that writes under the module root, so a `read only` module refuses the connection (clear server log "module is read only"; the client exits non-zero, nothing is transferred). A future pull/list operation can be opened up when it exists; the knob is already stored. - **`read only` safe default:** every network transfer FastSync currently supports is a push that writes under the module root, so a `read only` module refuses the connection (clear server log "module is read only"; the client exits non-zero, nothing is transferred). A future pull/list operation can be opened up when it exists; the knob is already stored.
+33 -19
View File
@@ -91,25 +91,39 @@ static bool parse_cidr(const char* cidr, int* prefix_out, uint8_t* bytes, int* f
return false; return false;
} }
/* A host pattern is valid when it is non-empty and, when it contains a '/', its /* A host pattern is valid when it is `*`, a valid IPv4/IPv6 literal, or a valid
* address/prefix halves parse as a CIDR. Literals, `*` and globs are accepted * CIDR. Peer addresses reaching the matcher are always numeric, so hostname
* as-is (a glob only ever matches a peer of the same shape). */ * globs are rejected at parse time: accepting one would create a deny rule that
* silently never matches (fail-open). */
static bool host_pattern_valid(const char* pattern) { static bool host_pattern_valid(const char* pattern) {
if (!pattern || *pattern == '\0') if (!pattern || *pattern == '\0')
return false; return false;
if (!strchr(pattern, '/')) if (strcmp(pattern, "*") == 0)
return true; return true;
uint8_t bytes[16]; if (strchr(pattern, '/')) {
int prefix; uint8_t bytes[16];
int family; int prefix;
return parse_cidr(pattern, &prefix, bytes, &family); int family;
return parse_cidr(pattern, &prefix, bytes, &family);
}
struct in_addr v4;
struct in6_addr v6;
return inet_pton(AF_INET, pattern, &v4) == 1 || inet_pton(AF_INET6, pattern, &v6) == 1;
} }
/* Append every comma- and/or whitespace-separated host pattern in `value` to /* Append every comma- and/or whitespace-separated host pattern in `value` to
* the heap-owned list. Returns false (err filled) on an invalid pattern or an * the heap-owned list (or replace the list when `replace` is set, which --dparam
* allocation failure. */ * uses so an override can narrow access rather than only widen it). Returns
* false (err filled) on an invalid pattern or an allocation failure. */
static bool store_host_list(char*** list, int* count, const char* value, const char* key, static bool store_host_list(char*** list, int* count, const char* value, const char* key,
const char* module_name, char* err, size_t err_size) { const char* module_name, bool replace, char* err, size_t err_size) {
if (replace) {
for (int i = 0; i < *count; i++)
free((*list)[i]);
free(*list);
*list = NULL;
*count = 0;
}
char* copy = str_dup(value); char* copy = str_dup(value);
if (!copy) { if (!copy) {
if (module_name) if (module_name)
@@ -278,8 +292,8 @@ static bool store_port(int* slot, const char* value, char* err, size_t err_size)
/* Apply a global scalar key/value. Keys are case-insensitive. Returns false /* Apply a global scalar key/value. Keys are case-insensitive. Returns false
* (err filled) on an unknown key or an invalid value. */ * (err filled) on an unknown key or an invalid value. */
static bool apply_global_key(DaemonConf* conf, char* key, const char* value, char* err, static bool apply_global_key(DaemonConf* conf, char* key, const char* value, bool replace_hosts,
size_t err_size) { char* err, size_t err_size) {
if (key_equals(key, "port")) if (key_equals(key, "port"))
return store_port(&conf->global.port, value, err, err_size); return store_port(&conf->global.port, value, err, err_size);
if (key_equals(key, "motd file")) { if (key_equals(key, "motd file")) {
@@ -302,10 +316,10 @@ static bool apply_global_key(DaemonConf* conf, char* key, const char* value, cha
return store_auth_failure_delay(&conf->global.auth_failure_delay_ms, value, err, err_size); return store_auth_failure_delay(&conf->global.auth_failure_delay_ms, value, err, err_size);
if (key_equals(key, "hosts allow")) if (key_equals(key, "hosts allow"))
return store_host_list(&conf->global.hosts_allow, &conf->global.hosts_allow_count, value, return store_host_list(&conf->global.hosts_allow, &conf->global.hosts_allow_count, value,
"hosts allow", NULL, err, err_size); "hosts allow", NULL, replace_hosts, err, err_size);
if (key_equals(key, "hosts deny")) if (key_equals(key, "hosts deny"))
return store_host_list(&conf->global.hosts_deny, &conf->global.hosts_deny_count, value, return store_host_list(&conf->global.hosts_deny, &conf->global.hosts_deny_count, value,
"hosts deny", NULL, err, err_size); "hosts deny", NULL, replace_hosts, err, err_size);
set_error(err, err_size, "unknown global key '%s'", key); set_error(err, err_size, "unknown global key '%s'", key);
return false; return false;
} }
@@ -389,10 +403,10 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
return store_max_connections(&module->max_connections, value, module->name, err, err_size); return store_max_connections(&module->max_connections, value, module->name, err, err_size);
if (key_equals(key, "hosts allow")) if (key_equals(key, "hosts allow"))
return store_host_list(&module->hosts_allow, &module->hosts_allow_count, value, "hosts allow", return store_host_list(&module->hosts_allow, &module->hosts_allow_count, value, "hosts allow",
module->name, err, err_size); false, module->name, err, err_size);
if (key_equals(key, "hosts deny")) if (key_equals(key, "hosts deny"))
return store_host_list(&module->hosts_deny, &module->hosts_deny_count, value, "hosts deny", return store_host_list(&module->hosts_deny, &module->hosts_deny_count, value, "hosts deny",
module->name, err, err_size); false, module->name, err, err_size);
set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name); set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name);
return false; return false;
} }
@@ -573,7 +587,7 @@ DaemonConf* daemon_conf_load(const char* path, char* err, size_t err_size) {
break; break;
} }
} else { } else {
if (!apply_global_key(conf, key, value, err, err_size)) { if (!apply_global_key(conf, key, value, false, err, err_size)) {
ok = false; ok = false;
break; break;
} }
@@ -628,7 +642,7 @@ int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err
set_error(err, err_size, "--dparam '%s' has an empty value", assignment); set_error(err, err_size, "--dparam '%s' has an empty value", assignment);
return -1; return -1;
} }
bool ok = apply_global_key(conf, key, value, err, err_size); bool ok = apply_global_key(conf, key, value, true, err, err_size);
free(copy); free(copy);
return ok ? 0 : -1; return ok ? 0 : -1;
} }
+3 -1
View File
@@ -94,7 +94,9 @@ typedef struct DaemonConf {
#define DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS 500 #define DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS 500
/* Largest accepted `auth failure delay`, so a typo cannot pin a connection /* Largest accepted `auth failure delay`, so a typo cannot pin a connection
* child in nanosleep for an absurd time. */ * child in nanosleep for an absurd time. */
#define DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS 60000 /* 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
/* Longest accepted config line (excluding the trailing newline). Longer lines /* Longest accepted config line (excluding the trailing newline). Longer lines
* are rejected rather than buffered unboundedly. */ * are rejected rather than buffered unboundedly. */
#define DAEMON_CONF_MAX_LINE 4096 #define DAEMON_CONF_MAX_LINE 4096
+13 -4
View File
@@ -603,12 +603,21 @@ bool utils_fd_peer_ip(int fd, char* buf, size_t len) {
return false; return false;
const void* src = NULL; const void* src = NULL;
int family = peer.ss_family; int family = peer.ss_family;
if (family == AF_INET) if (family == AF_INET) {
src = &((const struct sockaddr_in*)&peer)->sin_addr; src = &((const struct sockaddr_in*)&peer)->sin_addr;
else if (family == AF_INET6) } else if (family == AF_INET6) {
src = &((const struct sockaddr_in6*)&peer)->sin6_addr; const struct sockaddr_in6* peer6 = (const struct sockaddr_in6*)&peer;
else /* A dual-stack IPv6 listener reports IPv4 peers as ::ffff:a.b.c.d. Emit
* the IPv4 form so IPv4 ACL patterns (and logs) see the real address. */
if (IN6_IS_ADDR_V4MAPPED(&peer6->sin6_addr)) {
struct in_addr v4;
memcpy(&v4, &peer6->sin6_addr.s6_addr[12], sizeof(v4));
return inet_ntop(AF_INET, &v4, buf, (socklen_t)len) != NULL;
}
src = &peer6->sin6_addr;
} else {
return false; return false;
}
return inet_ntop(family, src, buf, (socklen_t)len) != NULL; return inet_ntop(family, src, buf, (socklen_t)len) != NULL;
} }
+11 -5
View File
@@ -321,6 +321,10 @@ static void test_daemon_conf_dparam_override() {
EXPECT_EQ_INT( EXPECT_EQ_INT(
daemon_conf_apply_dparam(conf, "hosts allow=127.0.0.1,10.0.0.0/8", err, sizeof(err)), 0); daemon_conf_apply_dparam(conf, "hosts allow=127.0.0.1,10.0.0.0/8", err, sizeof(err)), 0);
EXPECT_EQ_INT(conf->global.hosts_allow_count, 2); EXPECT_EQ_INT(conf->global.hosts_allow_count, 2);
/* A later --dparam replaces the list (an override must be able to narrow). */
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "hosts allow=127.0.0.1", err, sizeof(err)), 0);
EXPECT_EQ_INT(conf->global.hosts_allow_count, 1);
EXPECT_EQ_STR(conf->global.hosts_allow[0], "127.0.0.1");
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=notaport", err, sizeof(err)), -1); EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=notaport", err, sizeof(err)), -1);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "bogus=1", err, sizeof(err)), -1); EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "bogus=1", err, sizeof(err)), -1);
@@ -386,7 +390,7 @@ static void test_daemon_conf_limits_and_hosts_parse() {
char err[256]; char err[256];
EXPECT_EQ_INT(write_conf("max connections = 25\n" EXPECT_EQ_INT(write_conf("max connections = 25\n"
"auth failure delay = 0\n" "auth failure delay = 0\n"
"hosts allow = 10.0.0.0/8, *.example.com\n" "hosts allow = 10.0.0.0/8, 192.168.1.0/24\n"
"hosts deny = 192.168.0.1 2001:db8::/32\n" "hosts deny = 192.168.0.1 2001:db8::/32\n"
"\n" "\n"
"[m]\n" "[m]\n"
@@ -403,7 +407,7 @@ static void test_daemon_conf_limits_and_hosts_parse() {
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, 0); EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, 0);
EXPECT_EQ_INT(conf->global.hosts_allow_count, 2); 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[0], "10.0.0.0/8");
EXPECT_EQ_STR(conf->global.hosts_allow[1], "*.example.com"); EXPECT_EQ_STR(conf->global.hosts_allow[1], "192.168.1.0/24");
EXPECT_EQ_INT(conf->global.hosts_deny_count, 2); EXPECT_EQ_INT(conf->global.hosts_deny_count, 2);
EXPECT_EQ_STR(conf->global.hosts_deny[0], "192.168.0.1"); EXPECT_EQ_STR(conf->global.hosts_deny[0], "192.168.0.1");
EXPECT_EQ_STR(conf->global.hosts_deny[1], "2001:db8::/32"); EXPECT_EQ_STR(conf->global.hosts_deny[1], "2001:db8::/32");
@@ -415,9 +419,11 @@ static void test_daemon_conf_limits_and_hosts_parse() {
daemon_conf_free(conf); daemon_conf_free(conf);
const char* bad_values[] = { const char* bad_values[] = {
"max connections = 0\n", "max connections = -1\n", "max connections = abc\n", "max connections = 0\n", "max connections = -1\n",
"auth failure delay = -1\n", "auth failure delay = 70000\n", "auth failure delay = soon\n", "max connections = abc\n", "auth failure delay = -1\n",
"hosts allow = 10.0.0.0/99\n", "hosts deny = 2001:db8::/129\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",
}; };
for (size_t i = 0; i < sizeof(bad_values) / sizeof(bad_values[0]); i++) { for (size_t i = 0; i < sizeof(bad_values) / sizeof(bad_values[0]); i++) {
EXPECT_EQ_INT(write_conf(bad_values[i], &path), 0); EXPECT_EQ_INT(write_conf(bad_values[i], &path), 0);