Release v2.29.0 #312

Merged
TapTap merged 123 commits from dev into main 2026-09-23 02:05:14 +02:00
6 changed files with 322 additions and 23 deletions
Showing only changes of commit bb6c788cf9 - Show all commits
+18
View File
@@ -79,6 +79,24 @@ of 157 rows.
- **Daemon umask no longer forced to `0`.** `daemonize()` now sets the - **Daemon umask no longer forced to `0`.** `daemonize()` now sets the
conventional `022`, so implied parent directories created without `-p` are no conventional `022`, so implied parent directories created without `-p` are no
longer world-writable `0777`. longer world-writable `0777`.
- **Daemon modules are read-only by default.** A `--daemon` module is now
served read-only unless it sets `read only = no` (or rsync's `write only =
yes`), matching rsync: a real `rsyncd.conf` that omits `read only` is no
longer silently writable. A global `read only` still sets the default for
later modules, and an explicit module value wins. This is a behavior change
for existing FastSync-native configs that relied on the old writable default;
add `read only = no` to keep them writable. An rsync `write only = yes` is
mapped to writability (FastSync is push-only, so a module can never be read
from the network).
- **Accepted-but-unenforced rsync security keys now warn at startup.** The
rsync keys FastSync recognizes but does not implement — `secrets file`,
`refuse options`, `exclude`/`include`/`filter`, `max size`/`min size`,
`pre-xfer exec`/`post-xfer exec`, `incoming chmod`/`outgoing chmod`,
`name converter`, `use chroot`, `uid`/`gid`, and the rest of the
access-control set — load for migration compatibility but now emit a
`WARN` naming the key (and module) so an operator does not believe the
restriction is enforced. `auth users`/`secrets file` stay fail-closed: a
module declaring `auth users` still requires a FastSync credential store.
- **Credentials and signal handling hardened.** Secret files are opened with - **Credentials and signal handling hardened.** Secret files are opened with
`O_NOFOLLOW|O_NONBLOCK` (while allowing fd-backed store paths and bound-waiting `O_NOFOLLOW|O_NONBLOCK` (while allowing fd-backed store paths and bound-waiting
a FIFO read for ~3 s so a slow process substitution works but a connected-but- a FIFO read for ~3 s so a slow process substitution works but a connected-but-
+11 -3
View File
@@ -768,9 +768,17 @@ and `address`, the global section accepts:
- `hosts allow` / `hosts deny` — comma- and/or whitespace-separated host access - `hosts allow` / `hosts deny` — comma- and/or whitespace-separated host access
patterns. patterns.
A `[module]` requires `path`, and may also set `read only`, `client owner`, A `[module]` requires `path`, and may also set `read only`, `write only`,
`auth users`, `max connections` (0 = unlimited; enforced per module across all `client owner`, `auth users`, `max connections` (0 = unlimited; enforced per
connection children), and its own `hosts allow`/`hosts deny`. module across all connection children), and its own `hosts allow`/`hosts deny`.
Like rsync, a module is **read-only by default**: a bare `[module]` with only a
`path` refuses a write transfer. Opt a module into writability explicitly with
`read only = no` or `write only = yes`; a global `read only` value in the
section before the first `[module]` sets the default for later modules, and a
module's own `read only`/`write only = yes` always wins over it. An
rsync-style `write only = yes` is mapped to writability because FastSync is
push-only (a module can never be read from the network).
The per-host cap and the shared auth lockout identify a source by its numeric The per-host cap and the shared auth lockout identify a source by its numeric
peer IP. **Loopback peers (127.0.0.0/8, IPv6 `::1`) are exempt**: every local peer IP. **Loopback peers (127.0.0.0/8, IPv6 `::1`) are exempt**: every local
+91 -6
View File
@@ -1,5 +1,6 @@
#include "daemon_conf.h" #include "daemon_conf.h"
#include "credentials.h" #include "credentials.h"
#include "log.h"
#include "utils.h" #include "utils.h"
#include <arpa/inet.h> #include <arpa/inet.h>
#include <ctype.h> #include <ctype.h>
@@ -77,11 +78,13 @@ static const char* const kRsyncInertGlobalKeys[] = {
/* rsync 3.4.1 rsyncd.conf MODULE keys accepted in a [module] section that have /* rsync 3.4.1 rsyncd.conf MODULE keys accepted in a [module] section that have
* no FastSync equivalent (accepted-and-documented inert). Keys with a FastSync * no FastSync equivalent (accepted-and-documented inert). Keys with a FastSync
* meaning (`path`, `read only`, `auth users`, `max connections`, * meaning (`path`, `read only`, `write only`, `auth users`, `max connections`,
* `hosts allow`/`hosts deny`, `client owner`) are handled by apply_module_key * `hosts allow`/`hosts deny`, `client owner`) are handled by apply_module_key
* before this list is consulted. Security-relevant keys (`exclude`, `filter`, * before this list is consulted. Security-relevant keys (`exclude`, `filter`,
* `secrets file`, `refuse options`, ...) are inert, so a daemon-side filter or * `secrets file`, `refuse options`, ...) are inert, so a daemon-side filter or
* rsync secrets file is NOT enforced: see RSYNC_COMPAT.md for the residual. */ * rsync secrets file is NOT enforced: each is loudly warned about at load time
* (see kRsyncUnenforcedModuleSecurityKeys) and documented as a residual in
* RSYNC_COMPAT.md. */
static const char* const kRsyncInertModuleKeys[] = { static const char* const kRsyncInertModuleKeys[] = {
"comment", "comment",
"use chroot", "use chroot",
@@ -110,7 +113,6 @@ static const char* const kRsyncInertModuleKeys[] = {
"numeric ids", "numeric ids",
"fake super", "fake super",
"munge symlinks", "munge symlinks",
"write only",
"list", "list",
"dont compress", "dont compress",
"charset", "charset",
@@ -132,8 +134,57 @@ static const char* const kRsyncInertModuleKeys[] = {
"ignore nonreadable", "ignore nonreadable",
}; };
/* Subset of the inert rsync keys whose intent is access control (data
* visibility, credential source, transfer hooks, daemon privilege), plus the
* global keys that shape the daemon's privilege/identity. These load for
* rsync-config compatibility, but because FastSync ignores them an operator
* migrating a hardened rsyncd.conf must not believe the restriction applies.
* The loader emits one LOG_LEVEL_WARNING per occurrence naming the key (and the
* module, for a module key). `write only` is deliberately absent: it is mapped
* onto writability instead (FastSync is push-only, so a write-only module is
* simply writable). */
static const char* const kRsyncUnenforcedModuleSecurityKeys[] = {
"secrets file",
"auth digest",
"refuse options",
"exclude",
"include",
"exclude from",
"include from",
"filter",
"max size",
"min size",
"pre-xfer exec",
"post-xfer exec",
"incoming chmod",
"outgoing chmod",
"name converter",
"use chroot",
"daemon chroot",
"uid",
"gid",
"daemon uid",
"daemon gid",
"munge symlinks",
"fake super",
"strict modes",
"proxy protocol",
"proxy protocol hosts",
};
static const char* const kRsyncUnenforcedGlobalSecurityKeys[] = {
"use chroot",
"uid",
"gid",
"strict modes",
};
#define kRsyncInertGlobalCount (sizeof(kRsyncInertGlobalKeys) / sizeof(kRsyncInertGlobalKeys[0])) #define kRsyncInertGlobalCount (sizeof(kRsyncInertGlobalKeys) / sizeof(kRsyncInertGlobalKeys[0]))
#define kRsyncInertModuleCount (sizeof(kRsyncInertModuleKeys) / sizeof(kRsyncInertModuleKeys[0])) #define kRsyncInertModuleCount (sizeof(kRsyncInertModuleKeys) / sizeof(kRsyncInertModuleKeys[0]))
#define kRsyncUnenforcedModuleSecurityCount \
(sizeof(kRsyncUnenforcedModuleSecurityKeys) / sizeof(kRsyncUnenforcedModuleSecurityKeys[0]))
#define kRsyncUnenforcedGlobalSecurityCount \
(sizeof(kRsyncUnenforcedGlobalSecurityKeys) / sizeof(kRsyncUnenforcedGlobalSecurityKeys[0]))
static bool parse_bool_value(const char* value, bool* out) { static bool parse_bool_value(const char* value, bool* out) {
if (strcasecmp(value, "yes") == 0 || strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0) { if (strcasecmp(value, "yes") == 0 || strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0) {
@@ -352,7 +403,10 @@ DaemonConf* daemon_conf_create(void) {
if (!conf) if (!conf)
return NULL; return NULL;
conf->global.port = DAEMON_CONF_DEFAULT_PORT; conf->global.port = DAEMON_CONF_DEFAULT_PORT;
conf->global.read_only_default = false; /* rsync modules are READ-ONLY unless `read only = no` (or `write only = yes`)
* is set, so FastSync must default the same way: a migrated rsyncd.conf that
* omits `read only` is served read-only, never writable. */
conf->global.read_only_default = true;
conf->global.max_connections = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS; conf->global.max_connections = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS;
conf->global.auth_failure_delay_ms = DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS; 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.max_connections_per_host = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS_PER_HOST;
@@ -483,8 +537,14 @@ static bool apply_global_key(DaemonConf* conf, char* key, const char* value, boo
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, replace_hosts, err, err_size); "hosts deny", NULL, replace_hosts, err, err_size);
/* A recognized rsync global key with no FastSync equivalent loads inert. */ /* A recognized rsync global key with no FastSync equivalent loads inert. */
if (key_in_list(key, kRsyncInertGlobalKeys, kRsyncInertGlobalCount)) if (key_in_list(key, kRsyncInertGlobalKeys, kRsyncInertGlobalCount)) {
if (key_in_list(key, kRsyncUnenforcedGlobalSecurityKeys, kRsyncUnenforcedGlobalSecurityCount))
log_message(LOG_LEVEL_WARNING,
"daemon config: global key '%s' is accepted for rsync compatibility but is NOT "
"enforced by FastSync; the restriction it expresses will not be applied",
key);
return true; return true;
}
set_error(err, err_size, "unknown global key '%s'", key); set_error(err, err_size, "unknown global key '%s'", key);
return false; return false;
} }
@@ -516,6 +576,25 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
module->read_only_explicit = true; module->read_only_explicit = true;
return true; return true;
} }
/* rsync's `write only = yes` makes the module client-writable. FastSync has
* no read/pull path, so mapping it to writability is the exact
* security-relevant effect; set `read_only_explicit` so a global default
* cannot override the module's explicit choice. `write only = no` is the
* rsync default and leaves the module's read-only state untouched. */
if (key_equals(key, "write only")) {
bool parsed;
if (!parse_bool_value(value, &parsed)) {
set_error(err, err_size,
"module '%s': 'write only' must be yes/no (or true/false/1/0), got '%s'",
module->name, value);
return false;
}
if (parsed) {
module->read_only = false;
module->read_only_explicit = true;
}
return true;
}
if (key_equals(key, "client owner")) { if (key_equals(key, "client owner")) {
bool parsed; bool parsed;
if (!parse_bool_value(value, &parsed)) { if (!parse_bool_value(value, &parsed)) {
@@ -584,8 +663,14 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
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, false, err, err_size); module->name, false, err, err_size);
/* A recognized rsync module key with no FastSync equivalent loads inert. */ /* A recognized rsync module key with no FastSync equivalent loads inert. */
if (key_in_list(key, kRsyncInertModuleKeys, kRsyncInertModuleCount)) if (key_in_list(key, kRsyncInertModuleKeys, kRsyncInertModuleCount)) {
if (key_in_list(key, kRsyncUnenforcedModuleSecurityKeys, kRsyncUnenforcedModuleSecurityCount))
log_message(LOG_LEVEL_WARNING,
"daemon config: module '%s' key '%s' is accepted for rsync compatibility but is "
"NOT enforced by FastSync; the restriction it expresses will not be applied",
module->name, key);
return true; return true;
}
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;
} }
+17 -11
View File
@@ -21,12 +21,16 @@
* rsync compatibility: to reduce the divergence from rsync 3.4.1's rsyncd.conf * rsync compatibility: to reduce the divergence from rsync 3.4.1's rsyncd.conf
* grammar, the parser also ACCEPTS the common rsync GLOBAL and MODULE keys. * grammar, the parser also ACCEPTS the common rsync GLOBAL and MODULE keys.
* Keys with a FastSync equivalent are mapped onto it (the native spellings are * Keys with a FastSync equivalent are mapped onto it (the native spellings are
* unchanged). Keys with no FastSync equivalent are accepted and documented as * unchanged; `read only` defaults to yes like rsync, and `write only = yes`
* inert (they load successfully but have no effect) rather than failing the * opts a module into writability). Keys with no FastSync equivalent are
* whole config; the accepted inert set is listed in kRsyncInertGlobalKeys / * accepted and documented as inert (they load successfully but have no effect)
* kRsyncInertModuleKeys in daemon_conf.c and in RSYNC_COMPAT.md. A key * rather than failing the whole config; the accepted inert set is listed in
* outside both the FastSync-native grammar and the recognized rsync subset is * kRsyncInertGlobalKeys / kRsyncInertModuleKeys in daemon_conf.c and in
* still rejected as unknown. */ * RSYNC_COMPAT.md. Every inert key whose intent is access control is loudly
* warned about at load time (kRsyncUnenforced*SecurityKeys) so an operator
* migrating a hardened rsyncd.conf is never misled into believing the
* restriction is enforced. A key outside both the FastSync-native grammar and
* the recognized rsync subset is still rejected as unknown. */
/* A daemon module's configured root is used exactly like the standalone /* A daemon module's configured root is used exactly like the standalone
* server's --destination-root: the daemon confines every connection that * server's --destination-root: the daemon confines every connection that
@@ -55,10 +59,11 @@ typedef struct DaemonModule {
char* path; /* module root (daemon-side authorized root) */ char* path; /* module root (daemon-side authorized root) */
bool read_only; /* `read only = yes/no`; defaults to the global `read only` bool read_only; /* `read only = yes/no`; defaults to the global `read only`
default (rsync allows it in the global section), which is default (rsync allows it in the global section), which is
itself default no */ itself default YES (rsync modules are read-only unless
bool read_only_explicit; /* set when this module set its own `read only`, so a `read only = no` / `write only = yes` opts in) */
later global default (from a `--dparam read only=`) bool read_only_explicit; /* set when this module set its own `read only` or
does not override it */ `write only = yes`, so a later global default (from a
`--dparam read only=`) does not override it */
bool client_owner; /* `client owner = yes/no`; default no. Per-module opt-in bool client_owner; /* `client owner = yes/no`; default no. Per-module opt-in
that lets this module's clients choose ownership that lets this module's clients choose ownership
(--numeric-ids/--chown/--usermap/--groupmap/--fake-super/ (--numeric-ids/--chown/--usermap/--groupmap/--fake-super/
@@ -85,7 +90,8 @@ typedef struct DaemonConfGlobals {
char* address; /* `address` (optional bind address), may be NULL */ char* address; /* `address` (optional bind address), may be NULL */
bool read_only_default; /* global `read only` default for modules defined bool read_only_default; /* global `read only` default for modules defined
after it (rsync allows the module key in the after it (rsync allows the module key in the
global section); default no */ global section); default YES to match rsync's
read-only modules */
int max_connections; /* `max connections`, default int max_connections; /* `max connections`, default
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */ DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */
int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default
+13 -3
View File
@@ -229,6 +229,7 @@ def daemon_env():
"\n" "\n"
"[files]\n" "[files]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"\n" "\n"
"[readonly]\n" "[readonly]\n"
"path = %s\n" "path = %s\n"
@@ -236,18 +237,22 @@ def daemon_env():
"\n" "\n"
"[locked]\n" "[locked]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"auth users = alice\n" "auth users = alice\n"
"\n" "\n"
"[team]\n" "[team]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"auth users = alice,bob\n" "auth users = alice,bob\n"
"\n" "\n"
"[owner]\n" "[owner]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"client owner = yes\n" "client owner = yes\n"
"\n" "\n"
"[denied]\n" "[denied]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"hosts deny = 127.0.0.1\n" "hosts deny = 127.0.0.1\n"
% (config_port, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE, % (config_port, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE,
DENIED_MODULE)) DENIED_MODULE))
@@ -266,7 +271,8 @@ def daemon_env():
global DETACH_PORT global DETACH_PORT
DETACH_PORT = _find_free_port() DETACH_PORT = _find_free_port()
with open(DETACH_CONF, "w") as f: with open(DETACH_CONF, "w") as f:
f.write("port = %d\n\n[detach]\npath = %s\n" % (DETACH_PORT, DETACH_MODULE)) f.write("port = %d\n\n[detach]\npath = %s\nread only = no\n"
% (DETACH_PORT, DETACH_MODULE))
yield yield
_kill_by_cmdline_marker(DETACH_CONF) _kill_by_cmdline_marker(DETACH_CONF)
@@ -353,7 +359,8 @@ class TestDaemonModuleSelection:
the fix regresses.""" the fix regresses."""
port = _find_free_port() port = _find_free_port()
with open(UMASK_CONF, "w") as f: with open(UMASK_CONF, "w") as f:
f.write("port = %d\n\n[files]\npath = %s\n" % (port, FILES_MODULE)) f.write("port = %d\n\n[files]\npath = %s\nread only = no\n"
% (port, FILES_MODULE))
sub = os.path.join(FILES_MODULE, "umask_check") sub = os.path.join(FILES_MODULE, "umask_check")
shutil.rmtree(sub, ignore_errors=True) shutil.rmtree(sub, ignore_errors=True)
os.makedirs(sub, exist_ok=True) os.makedirs(sub, exist_ok=True)
@@ -1128,7 +1135,8 @@ class TestDaemonMotd:
motd_line = "motd file = %s\n" % motd_path if motd_path else "" motd_line = "motd file = %s\n" % motd_path if motd_path else ""
os.makedirs(self.MOTD_MODULE, exist_ok=True) os.makedirs(self.MOTD_MODULE, exist_ok=True)
with open(self.MOTD_CONF, "w") as f: with open(self.MOTD_CONF, "w") as f:
f.write("port = %d\n%s\n[files]\npath = %s\n" % (port, motd_line, self.MOTD_MODULE)) f.write("port = %d\n%s\n[files]\npath = %s\nread only = no\n"
% (port, motd_line, self.MOTD_MODULE))
d = DaemonManager() d = DaemonManager()
d.start(self.MOTD_CONF, port_override=port) d.start(self.MOTD_CONF, port_override=port)
return d, port return d, port
@@ -1365,6 +1373,7 @@ class TestDaemonConnectionLimits:
"\n" "\n"
"[locked]\n" "[locked]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"auth users = alice\n" "auth users = alice\n"
% (port, AUTH_MODULE)) % (port, AUTH_MODULE))
d = DaemonManager() d = DaemonManager()
@@ -1402,6 +1411,7 @@ class TestDaemonConnectionLimits:
"\n" "\n"
"[files]\n" "[files]\n"
"path = %s\n" "path = %s\n"
"read only = no\n"
"max connections = 2\n" "max connections = 2\n"
% (port, FILES_MODULE)) % (port, FILES_MODULE))
d = DaemonManager() d = DaemonManager()
+172
View File
@@ -1,6 +1,7 @@
#include "test_daemon_conf.h" #include "test_daemon_conf.h"
#include "credentials.h" #include "credentials.h"
#include "daemon_conf.h" #include "daemon_conf.h"
#include "log.h"
#include "test_utils.h" #include "test_utils.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -31,6 +32,9 @@ static void test_daemon_conf_create_defaults() {
EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT); EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT);
EXPECT_NULL(conf->global.motd_file); EXPECT_NULL(conf->global.motd_file);
EXPECT_NULL(conf->global.address); EXPECT_NULL(conf->global.address);
/* rsync modules are read-only unless they opt in, so the default must be
* true. */
EXPECT_TRUE(conf->global.read_only_default);
EXPECT_EQ_INT(conf->global.max_connections, DAEMON_CONF_DEFAULT_MAX_CONNECTIONS); 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.auth_failure_delay_ms, DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS);
EXPECT_EQ_INT(conf->global.max_connections_per_host, EXPECT_EQ_INT(conf->global.max_connections_per_host,
@@ -802,6 +806,172 @@ static void test_daemon_conf_dparam_rsync_keys() {
daemon_conf_free(conf); daemon_conf_free(conf);
} }
/* rsync modules default to READ-ONLY; `read only = no` / `write only = yes`
* opt a module into writability, and an explicit module value wins over a
* global default. */
static void test_daemon_conf_read_only_default_and_opt_in() {
char* path;
char err[256];
DaemonConf* conf;
/* A module that never mentions read only/write only is READ-ONLY, matching
* rsync (a migrated rsyncd.conf must not be served writable). */
EXPECT_EQ_INT(write_conf("[m]\npath = /x\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_TRUE(conf->modules[0].read_only);
EXPECT_FALSE(conf->modules[0].read_only_explicit);
daemon_conf_free(conf);
/* `read only = no` opts in to writable. */
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nread only = no\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_FALSE(conf->modules[0].read_only);
EXPECT_TRUE(conf->modules[0].read_only_explicit);
daemon_conf_free(conf);
/* `write only = yes` opts in to writable (FastSync is push-only). */
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nwrite only = yes\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_FALSE(conf->modules[0].read_only);
EXPECT_TRUE(conf->modules[0].read_only_explicit);
daemon_conf_free(conf);
/* `write only = no` is rsync's default and does not undo read-only. */
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nwrite only = no\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_TRUE(conf->modules[0].read_only);
EXPECT_FALSE(conf->modules[0].read_only_explicit);
daemon_conf_free(conf);
/* A global `read only = no` is the default for later modules; an explicit
* module `read only`/`write only = yes` still wins. */
EXPECT_EQ_INT(write_conf("read only = no\n[a]\npath = /a\n"
"[b]\npath = /b\nread only = yes\n"
"[c]\npath = /c\nwrite only = yes\n",
&path),
0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_FALSE(conf->global.read_only_default);
EXPECT_FALSE(conf->modules[0].read_only);
EXPECT_TRUE(conf->modules[1].read_only);
EXPECT_FALSE(conf->modules[2].read_only);
daemon_conf_free(conf);
/* A global `read only = yes` keeps modules without an explicit value
* read-only. */
EXPECT_EQ_INT(write_conf("read only = yes\n[a]\npath = /a\n"
"[b]\npath = /b\nread only = no\n"
"[c]\npath = /c\nwrite only = yes\n",
&path),
0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_TRUE(conf->global.read_only_default);
EXPECT_TRUE(conf->modules[0].read_only);
EXPECT_FALSE(conf->modules[1].read_only);
EXPECT_FALSE(conf->modules[2].read_only);
daemon_conf_free(conf);
/* An invalid `write only` value is a clear parse error. */
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nwrite only = maybe\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "write only") != NULL);
}
/* Security-relevant rsync keys are accepted for migration but have no FastSync
* effect, so loading must warn loudly (naming the key and module) rather than
* letting an operator believe the restriction is enforced. */
static void test_daemon_conf_unenforced_security_keys_warned() {
char* path;
char err[256];
EXPECT_EQ_INT(write_conf("use chroot = yes\n"
"uid = nobody\n"
"[m]\n"
"path = /x\n"
"read only = no\n"
"secrets file = /etc/rsyncd.secrets\n"
"refuse options = delete\n"
"exclude = *.tmp\n"
"max size = 1M\n"
"pre-xfer exec = /bin/true\n"
"incoming chmod = F644\n"
"name converter = sh\n",
&path),
0);
set_log_level(LOG_LEVEL_WARNING);
FILE* log_capture = tmpfile();
EXPECT_NOT_NULL(log_capture);
log_set_file(log_capture);
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
fflush(log_capture);
rewind(log_capture);
log_set_file(NULL);
free(path);
/* Inert security keys must never fail the load. */
EXPECT_NOT_NULL(conf);
EXPECT_FALSE(conf->modules[0].read_only);
bool saw_secrets = false;
bool saw_refuse = false;
bool saw_filter = false;
bool saw_size = false;
bool saw_hook = false;
bool saw_chmod = false;
bool saw_converter = false;
bool saw_global_chroot = false;
bool saw_global_uid = false;
char line[512];
while (fgets(line, sizeof(line), log_capture) != NULL) {
if (strstr(line, "NOT enforced") == NULL)
continue;
if (strstr(line, "module 'm'") && strstr(line, "secrets file"))
saw_secrets = true;
if (strstr(line, "module 'm'") && strstr(line, "refuse options"))
saw_refuse = true;
if (strstr(line, "module 'm'") && strstr(line, "exclude"))
saw_filter = true;
if (strstr(line, "module 'm'") && strstr(line, "max size"))
saw_size = true;
if (strstr(line, "module 'm'") && strstr(line, "pre-xfer exec"))
saw_hook = true;
if (strstr(line, "module 'm'") && strstr(line, "incoming chmod"))
saw_chmod = true;
if (strstr(line, "module 'm'") && strstr(line, "name converter"))
saw_converter = true;
if (strstr(line, "global key 'use chroot'"))
saw_global_chroot = true;
if (strstr(line, "global key 'uid'"))
saw_global_uid = true;
}
fclose(log_capture);
EXPECT_TRUE(saw_secrets);
EXPECT_TRUE(saw_refuse);
EXPECT_TRUE(saw_filter);
EXPECT_TRUE(saw_size);
EXPECT_TRUE(saw_hook);
EXPECT_TRUE(saw_chmod);
EXPECT_TRUE(saw_converter);
EXPECT_TRUE(saw_global_chroot);
EXPECT_TRUE(saw_global_uid);
daemon_conf_free(conf);
}
void test_daemon_conf() { void test_daemon_conf() {
test_daemon_conf_create_defaults(); test_daemon_conf_create_defaults();
test_daemon_conf_full_parse(); test_daemon_conf_full_parse();
@@ -826,4 +996,6 @@ void test_daemon_conf() {
test_daemon_conf_rsync_unknown_keys_rejected(); test_daemon_conf_rsync_unknown_keys_rejected();
test_daemon_conf_rsync_read_only_invalid(); test_daemon_conf_rsync_read_only_invalid();
test_daemon_conf_dparam_rsync_keys(); test_daemon_conf_dparam_rsync_keys();
test_daemon_conf_read_only_default_and_opt_in();
test_daemon_conf_unenforced_security_keys_warned();
} }