Merge branch 'quality/g324' into quality/refactor

This commit is contained in:
2026-09-24 02:35:33 +02:00
+169 -129
View File
@@ -1306,29 +1306,45 @@ static bool daemonize(void) {
return true; return true;
} }
int main(int argc, char* argv[]) { /* Apply the process-wide policies shared by the stdio and listener
/* Capture the process umask now, while still single-threaded: the cached * entrypoints: logging verbosity, signal handling, the parsed server
* value is what file_mode_base() uses, and reading it later would race with * authorization policies, and the socket timeout floor. Runs after CLI
* receiver threads creating files. */ * parsing and after the standalone --hash-credentials tool has been ruled
file_umask_capture(); * out. */
ServerCliOptions opts; static void configure_server_process(const ServerCliOptions* opts) {
char cli_err[512]; signal(SIGPIPE, SIG_IGN);
int parse_result = server_cli_parse(argc, argv, &opts, cli_err, sizeof(cli_err)); if (opts->verbose) {
if (parse_result == 1) { set_log_level(LOG_LEVEL_DEBUG);
print_server_usage(); set_log_debug_flags(LOG_DEBUG_ALL);
return 0;
}
if (parse_result < 0) {
server_cli_options_free(&opts);
fprintf(stderr, "Error: %s\n", cli_err);
print_server_usage();
return 1;
} }
if (opts->tls_ca && !opts->use_tls)
log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls");
/* Persist the parsed server policies into the process-global policy state
* BEFORE the stdio branch: an SSH-launched `--stdio` server (whose argv came
* from the client via --remote-option and friends) must honor --allow-delete,
* --trust-sender and --client-cn exactly like the standalone listener. */
required_client_cn = opts->client_cn;
allow_delete = opts->allow_delete;
trust_sender = opts->trust_sender;
allow_unauthenticated = opts->allow_unauthenticated;
server_no_super = opts->no_super;
/* --stdio rejects --allow-super at parse time; force it off here as well so
* this process-global policy cannot be re-enabled by a future caller. */
server_allow_super = opts->allow_super && !opts->stdio_mode;
server_iconv_spec = opts->iconv_spec;
install_cleanup_handler(SIGINT);
install_cleanup_handler(SIGTERM);
/* Server-owned socket deadline floor: the client default --timeout=0 would
* otherwise leave accepted sockets without SO_RCVTIMEO/SO_SNDTIMEO and let a
* silent peer hold a connection (and its process slot) forever. */
tcp_set_timeouts(SERVER_IO_TIMEOUT_SEC, SERVER_IO_TIMEOUT_SEC);
}
/* --hash-credentials: standalone offline tool; read user:password lines and /* --hash-credentials: standalone offline tool; read user:password lines and
* emit new-format credential-store lines, then exit. */ * emit new-format credential-store lines, then exit. Consumes and frees
if (opts.hash_credentials_file) { * opts. */
uint32_t iters = opts.hash_iterations_set ? opts.hash_iterations : CREDENTIAL_DEFAULT_ITERS; static int run_hash_credentials_tool(ServerCliOptions* opts) {
uint32_t iters = opts->hash_iterations_set ? opts->hash_iterations : CREDENTIAL_DEFAULT_ITERS;
/* The output is secret material: if it is redirected to a regular file, /* The output is secret material: if it is redirected to a regular file,
* warn when that file is group/other-accessible (the store must be 0600). */ * warn when that file is group/other-accessible (the store must be 0600). */
struct stat out_st; struct stat out_st;
@@ -1338,53 +1354,29 @@ int main(int argc, char* argv[]) {
"Warning: credential-store output is a group/other-accessible file; restrict it to " "Warning: credential-store output is a group/other-accessible file; restrict it to "
"mode 0600 (chmod 600)\n"); "mode 0600 (chmod 600)\n");
char hash_err[512]; char hash_err[512];
if (credentials_hash_file(opts.hash_credentials_file, iters, stdout, hash_err, if (credentials_hash_file(opts->hash_credentials_file, iters, stdout, hash_err,
sizeof(hash_err)) != 0) { sizeof(hash_err)) != 0) {
fprintf(stderr, "Error: %s\n", hash_err); fprintf(stderr, "Error: %s\n", hash_err);
server_cli_options_free(&opts); server_cli_options_free(opts);
return 1; return 1;
} }
server_cli_options_free(&opts); server_cli_options_free(opts);
return 0; return 0;
} }
int exit_code = 0; /* SSH --stdio session: the transport is authenticated by sshd outside of
signal(SIGPIPE, SIG_IGN); * FastSync, so the single connection is served over STDIN/STDOUT and the
if (opts.verbose) { * process exits. The destination root is authorized exactly like the listener
set_log_level(LOG_LEVEL_DEBUG); * path. Consumes and frees opts. */
set_log_debug_flags(LOG_DEBUG_ALL); static int run_stdio_server(ServerCliOptions* opts) {
}
if (opts.tls_ca && !opts.use_tls)
log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls");
/* Persist the parsed server policies into the process-global policy state
* BEFORE the stdio branch: an SSH-launched `--stdio` server (whose argv came
* from the client via --remote-option and friends) must honor --allow-delete,
* --trust-sender and --client-cn exactly like the standalone listener. */
required_client_cn = opts.client_cn;
allow_delete = opts.allow_delete;
trust_sender = opts.trust_sender;
allow_unauthenticated = opts.allow_unauthenticated;
server_no_super = opts.no_super;
/* --stdio rejects --allow-super at parse time; force it off here as well so
* this process-global policy cannot be re-enabled by a future caller. */
server_allow_super = opts.allow_super && !opts.stdio_mode;
server_iconv_spec = opts.iconv_spec;
install_cleanup_handler(SIGINT);
install_cleanup_handler(SIGTERM);
/* Server-owned socket deadline floor: the client default --timeout=0 would
* otherwise leave accepted sockets without SO_RCVTIMEO/SO_SNDTIMEO and let a
* silent peer hold a connection (and its process slot) forever. */
tcp_set_timeouts(SERVER_IO_TIMEOUT_SEC, SERVER_IO_TIMEOUT_SEC);
if (opts.stdio_mode) {
/* SSH authenticates the stdio transport outside of FastSync. */ /* SSH authenticates the stdio transport outside of FastSync. */
allow_unauthenticated = true; allow_unauthenticated = true;
if (!configure_authorization(opts.destination_root)) { if (!configure_authorization(opts->destination_root)) {
char* escaped = output_escape(opts.destination_root, false); char* escaped = output_escape(opts->destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n", fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>"); escaped ? escaped : "<allocation failed>");
free(escaped); free(escaped);
server_cli_options_free(&opts); server_cli_options_free(opts);
return 1; return 1;
} }
io_set_fds(STDIN_FILENO, STDOUT_FILENO); io_set_fds(STDIN_FILENO, STDOUT_FILENO);
@@ -1393,34 +1385,32 @@ int main(int argc, char* argv[]) {
* and are released by process exit. */ * and are released by process exit. */
handler(STDIN_FILENO); handler(STDIN_FILENO);
release_authorization(); release_authorization();
server_cli_options_free(&opts); server_cli_options_free(opts);
return 0; return 0;
} }
int port = opts.port; /* Load the daemon config, apply --dparam overrides, resolve the effective
int bind_family = opts.bind_family; * port/address, and surface the operator-facing module warnings. On failure
const char* bind_address = opts.bind_address; * the error is printed and false is returned. */
static bool load_daemon_policy(ServerCliOptions* opts, int* port, const char** bind_address,
if (opts.daemon_mode) { char* err, size_t err_size) {
const char* config_path = opts.config_path ? opts.config_path : default_daemon_config_path(); const char* config_path = opts->config_path ? opts->config_path : default_daemon_config_path();
g_daemon_conf = daemon_conf_load(config_path, cli_err, sizeof(cli_err)); g_daemon_conf = daemon_conf_load(config_path, err, err_size);
if (!g_daemon_conf) { if (!g_daemon_conf) {
server_cli_options_free(&opts); fprintf(stderr, "Error: %s\n", err);
fprintf(stderr, "Error: %s\n", cli_err); return false;
return 1;
} }
for (int i = 0; i < opts.dparam_count; i++) { for (int i = 0; i < opts->dparam_count; i++) {
if (daemon_conf_apply_dparam(g_daemon_conf, opts.dparams[i], cli_err, sizeof(cli_err)) != 0) { if (daemon_conf_apply_dparam(g_daemon_conf, opts->dparams[i], err, err_size) != 0) {
fprintf(stderr, "Error: --dparam: %s\n", cli_err); fprintf(stderr, "Error: --dparam: %s\n", err);
exit_code = 1; return false;
goto out;
} }
} }
/* Effective port: -p (highest) > --dparam port > config port (default 873). */ /* Effective port: -p (highest) > --dparam port > config port (default 873). */
if (!opts.port_set) if (!opts->port_set)
port = g_daemon_conf->global.port; *port = g_daemon_conf->global.port;
if (!bind_address) if (!*bind_address)
bind_address = g_daemon_conf->global.address; *bind_address = g_daemon_conf->global.address;
if (g_daemon_conf->module_count == 0) if (g_daemon_conf->module_count == 0)
log_message(LOG_LEVEL_WARNING, log_message(LOG_LEVEL_WARNING,
"daemon config has no modules; every connection will be refused"); "daemon config has no modules; every connection will be refused");
@@ -1441,20 +1431,23 @@ int main(int argc, char* argv[]) {
"across all connection children)", "across all connection children)",
g_daemon_conf->modules[i].name, g_daemon_conf->modules[i].max_connections); g_daemon_conf->modules[i].name, g_daemon_conf->modules[i].max_connections);
} }
/* Daemon credential store (Wave B). --password-file and --early-input return true;
* feed the same store, loaded BEFORE the listener forks so every }
* connection child shares one read-only store. Fail closed at startup: a
* module that declares `auth users` without a store (or with an empty /* Load the daemon credential store (Wave B) and enforce the fail-closed
* store) refuses to start rather than serving a module whose credentials * startup check: a module that declares `auth users` without a store (or with
* can never be verified. */ * an empty store) refuses to start rather than serving a module whose
g_credentials = * credentials can never be verified. On failure the error is printed and
credentials_load(opts.password_file, opts.early_input_file, cli_err, sizeof(cli_err)); * false is returned. */
static bool validate_daemon_credentials(const ServerCliOptions* opts, char* err, size_t err_size) {
/* --password-file and --early-input feed the same store, loaded BEFORE the
* listener forks so every connection child shares one read-only store. */
g_credentials = credentials_load(opts->password_file, opts->early_input_file, err, err_size);
if (!g_credentials) { if (!g_credentials) {
server_cli_options_free(&opts); fprintf(stderr, "Error: %s\n", err);
fprintf(stderr, "Error: %s\n", cli_err); return false;
return 1;
} }
bool credential_source_given = opts.password_file != NULL || opts.early_input_file != NULL; bool credential_source_given = opts->password_file != NULL || opts->early_input_file != NULL;
for (int i = 0; i < g_daemon_conf->module_count; i++) { for (int i = 0; i < g_daemon_conf->module_count; i++) {
const DaemonModule* module = &g_daemon_conf->modules[i]; const DaemonModule* module = &g_daemon_conf->modules[i];
if (module->auth_user_count == 0) if (module->auth_user_count == 0)
@@ -1464,16 +1457,14 @@ int main(int argc, char* argv[]) {
"Error: module '%s' declares 'auth users' but no credential store was given " "Error: module '%s' declares 'auth users' but no credential store was given "
"(--password-file or --early-input); refusing to start (fail closed)\n", "(--password-file or --early-input); refusing to start (fail closed)\n",
module->name); module->name);
server_cli_options_free(&opts); return false;
return 1;
} }
if (credentials_store_size(g_credentials) == 0) { if (credentials_store_size(g_credentials) == 0) {
fprintf(stderr, fprintf(stderr,
"Error: module '%s' declares 'auth users' but the credential store is empty; " "Error: module '%s' declares 'auth users' but the credential store is empty; "
"refusing to start (fail closed)\n", "refusing to start (fail closed)\n",
module->name); module->name);
server_cli_options_free(&opts); return false;
return 1;
} }
for (int j = 0; j < module->auth_user_count; j++) { for (int j = 0; j < module->auth_user_count; j++) {
if (!credentials_store_has(g_credentials, module->auth_users[j])) if (!credentials_store_has(g_credentials, module->auth_users[j]))
@@ -1483,32 +1474,30 @@ int main(int argc, char* argv[]) {
module->name, module->auth_users[j]); module->name, module->auth_users[j]);
} }
} }
/* Shared cross-process registry for the per-module / per-source caps and return true;
* the auth lockout. Created HERE in the parent before any accept-loop }
* fork; every connection child inherits the mapping. A failure degrades to
* "registry disabled" (the global cap and host ACLs still apply) rather /* Create the shared cross-process registry for the per-module / per-source
* than refusing to start. */ * caps and the auth lockout. Called in the parent before any accept-loop fork;
g_daemon_limits = daemon_limits_create((int)g_daemon_conf->global.max_connections, * every connection child inherits the mapping. A failure degrades to
g_daemon_conf->module_count, * "registry disabled" (the global cap and host ACLs still apply) rather than
g_daemon_conf->global.max_connections_per_host, * refusing to start. */
g_daemon_conf->global.auth_lockout_threshold, static void create_daemon_limits(void) {
g_daemon_limits = daemon_limits_create(
(int)g_daemon_conf->global.max_connections, g_daemon_conf->module_count,
g_daemon_conf->global.max_connections_per_host, g_daemon_conf->global.auth_lockout_threshold,
g_daemon_conf->global.auth_lockout_duration_sec); g_daemon_conf->global.auth_lockout_duration_sec);
if (!g_daemon_limits) if (!g_daemon_limits)
log_message(LOG_LEVEL_WARNING, log_message(LOG_LEVEL_WARNING,
"daemon: could not allocate the shared connection registry; per-module / " "daemon: could not allocate the shared connection registry; per-module / "
"per-host caps and the cross-process auth lockout are disabled (the global " "per-host caps and the cross-process auth lockout are disabled (the global "
"'max connections' cap and host ACLs still apply)"); "'max connections' cap and host ACLs still apply)");
} else { }
if (!configure_authorization(opts.destination_root)) {
char* escaped = output_escape(opts.destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>");
free(escaped);
server_cli_options_free(&opts);
return 1;
}
}
/* Bind the listener, apply the daemon caps, set up TLS when requested, detach
* when daemonizing, and run the accept loop. Returns the process exit code. */
static int start_listener(ServerCliOptions* opts, int port, int bind_family,
const char* bind_address) {
ServerBindOptions bind_opts; ServerBindOptions bind_opts;
bind_opts.bind_address = bind_address; bind_opts.bind_address = bind_address;
bind_opts.family = bind_family; bind_opts.family = bind_family;
@@ -1516,50 +1505,73 @@ int main(int argc, char* argv[]) {
if (!g_server) { if (!g_server) {
log_message(LOG_LEVEL_ERROR, "Failed to create server"); log_message(LOG_LEVEL_ERROR, "Failed to create server");
release_authorization(); release_authorization();
exit_code = 1; return 1;
goto out;
} }
if (g_daemon_conf) if (g_daemon_conf)
server_set_max_connections(g_server, (unsigned int)g_daemon_conf->global.max_connections); server_set_max_connections(g_server, (unsigned int)g_daemon_conf->global.max_connections);
if (g_daemon_limits) if (g_daemon_limits)
server_set_limit_registry(g_server, g_daemon_limits); server_set_limit_registry(g_server, g_daemon_limits);
if (opts.use_tls) { if (opts->use_tls) {
if (!opts.tls_cert || !opts.tls_key || !opts.tls_ca || !opts.client_cn) { if (!opts->tls_cert || !opts->tls_key || !opts->tls_ca || !opts->client_cn) {
fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n"); fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n");
server_delete(&g_server); server_delete(&g_server);
release_authorization(); release_authorization();
exit_code = 1; return 1;
goto out;
} }
tls_global_init(); tls_global_init();
if (!server_create_tls(g_server, opts.tls_cert, opts.tls_key, opts.tls_ca)) { if (!server_create_tls(g_server, opts->tls_cert, opts->tls_key, opts->tls_ca)) {
log_message(LOG_LEVEL_ERROR, "Failed to set up TLS"); log_message(LOG_LEVEL_ERROR, "Failed to set up TLS");
server_delete(&g_server); server_delete(&g_server);
release_authorization(); release_authorization();
exit_code = 1; return 1;
goto out;
} }
} }
/* Detach after the listening socket (and TLS context) exist so the /* Detach after the listening socket (and TLS context) exist so the
* background daemon inherits a fully-bound listener. --no-detach runs in * background daemon inherits a fully-bound listener. --no-detach runs in
* the foreground, which is how tests drive the daemon. */ * the foreground, which is how tests drive the daemon. */
if (opts.daemon_mode && !opts.no_detach) { if (opts->daemon_mode && !opts->no_detach) {
if (!daemonize()) { if (!daemonize()) {
log_message(LOG_LEVEL_ERROR, "Failed to daemonize"); log_message(LOG_LEVEL_ERROR, "Failed to daemonize");
server_delete(&g_server); server_delete(&g_server);
release_authorization(); release_authorization();
exit_code = 1; return 1;
goto out;
} }
} }
if (opts->use_tls)
if (opts.use_tls)
server_listen_tls(g_server, handler); server_listen_tls(g_server, handler);
else else
server_listen(g_server, handler); server_listen(g_server, handler);
server_delete(&g_server); server_delete(&g_server);
release_authorization(); release_authorization();
return 0;
}
/* Listener entrypoint: the daemon (config-driven, possibly detached) and the
* standalone TCP server share the same bind/TLS/listen path. Consumes and
* frees opts. */
static int run_daemon_server(ServerCliOptions* opts) {
int port = opts->port;
const char* bind_address = opts->bind_address;
char cli_err[512];
int exit_code = 0;
if (opts->daemon_mode) {
if (!load_daemon_policy(opts, &port, &bind_address, cli_err, sizeof(cli_err)) ||
!validate_daemon_credentials(opts, cli_err, sizeof(cli_err))) {
exit_code = 1;
goto out;
}
create_daemon_limits();
} else if (!configure_authorization(opts->destination_root)) {
char* escaped = output_escape(opts->destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>");
free(escaped);
exit_code = 1;
goto out;
}
exit_code = start_listener(opts, port, opts->bind_family, bind_address);
out: out:
daemon_limits_destroy(g_daemon_limits); daemon_limits_destroy(g_daemon_limits);
@@ -1568,7 +1580,35 @@ out:
g_daemon_conf = NULL; g_daemon_conf = NULL;
credentials_free(g_credentials); credentials_free(g_credentials);
g_credentials = NULL; g_credentials = NULL;
server_cli_options_free(&opts); server_cli_options_free(opts);
return exit_code; return exit_code;
} }
int main(int argc, char* argv[]) {
/* Capture the process umask now, while still single-threaded: the cached
* value is what file_mode_base() uses, and reading it later would race with
* receiver threads creating files. */
file_umask_capture();
ServerCliOptions opts;
char cli_err[512];
int parse_result = server_cli_parse(argc, argv, &opts, cli_err, sizeof(cli_err));
if (parse_result == 1) {
print_server_usage();
return 0;
}
if (parse_result < 0) {
server_cli_options_free(&opts);
fprintf(stderr, "Error: %s\n", cli_err);
print_server_usage();
return 1;
}
if (opts.hash_credentials_file)
return run_hash_credentials_tool(&opts);
configure_server_process(&opts);
if (opts.stdio_mode)
return run_stdio_server(&opts);
return run_daemon_server(&opts);
}
#endif #endif