Merge feat/p5-socket: --address, -4/-6, --sockopts, server bind options

# Conflicts:
#	RSYNC_COMPAT.md
#	tests/test_client_cli.c
This commit is contained in:
2026-09-09 14:30:36 +02:00
15 changed files with 711 additions and 49 deletions
+113
View File
@@ -11,6 +11,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
static void config_set_defaults(Config* config) {
config->version = str_dup(PROTOCOL_VERSION);
@@ -134,6 +136,8 @@ static void config_set_defaults(Config* config) {
config->bind_address = NULL;
config->ipv6 = false;
config->ipv4 = false;
config->sockopts = NULL;
config->sockopt_count = 0;
config->daemon = false;
config->daemon_config = NULL;
config->server_mode = false;
@@ -340,6 +344,114 @@ int config_basis_append(Config* config, BasisDestType type, const char* path) {
return 0;
}
/* Strict --sockopts allowlist: map an option NAME to its SockOptId, or -1 when
* the name is not on the allowlist. The list is intentionally closed so an
* unknown option is an error, never a silent no-op. */
static int sockopt_id_from_name(const char* name) {
if (strcmp(name, "TCP_NODELAY") == 0)
return SOCKOPT_TCP_NODELAY;
if (strcmp(name, "SO_KEEPALIVE") == 0)
return SOCKOPT_SO_KEEPALIVE;
if (strcmp(name, "SO_RCVBUF") == 0)
return SOCKOPT_SO_RCVBUF;
if (strcmp(name, "SO_SNDBUF") == 0)
return SOCKOPT_SO_SNDBUF;
if (strcmp(name, "SO_REUSEADDR") == 0)
return SOCKOPT_SO_REUSEADDR;
return -1;
}
static bool sockopt_is_boolean(SockOptId id) {
return id == SOCKOPT_TCP_NODELAY || id == SOCKOPT_SO_KEEPALIVE || id == SOCKOPT_SO_REUSEADDR;
}
/* Parse one SockOptId's value. Booleans accept only 0/1 (a numeric "on" is
* rejected rather than coerced); buffer sizes accept any non-negative int.
* Returns 0 on success, -1 on a bad value. */
static int sockopt_parse_value(SockOptId id, const char* value, int* out) {
if (sockopt_is_boolean(id)) {
if (strcmp(value, "0") == 0) {
*out = 0;
return 0;
}
if (strcmp(value, "1") == 0) {
*out = 1;
return 0;
}
return -1;
}
if (!value || *value == '\0')
return -1;
char* end;
errno = 0;
long v = strtol(value, &end, 10);
if (errno != 0 || *end != '\0' || v < 0 || v > INT_MAX)
return -1;
*out = (int)v;
return 0;
}
int config_sockopts_parse(const char* spec, SockOptEntry** out, int* out_count) {
if (!spec || *spec == '\0' || !out || !out_count)
return -1;
char* copy = str_dup(spec);
if (!copy)
return -1;
int count = 0;
int capacity = 0;
SockOptEntry* entries = NULL;
char* saveptr = NULL;
bool ok = true;
for (const char* token = strtok_r(copy, ",", &saveptr); token != NULL;
token = strtok_r(NULL, ",", &saveptr)) {
if (*token == '\0') {
ok = false; /* empty entry: a stray/trailing comma */
break;
}
char* eq = strchr(token, '=');
if (eq)
*eq = '\0';
int id = sockopt_id_from_name(token);
if (id < 0) {
ok = false; /* unknown option name */
break;
}
int val;
/* rsync's --sockopts are OPT=VAL; a value is required for every option, so
* a bare option name (no '=') is rejected rather than coerced. */
if (eq == NULL || eq[1] == '\0') {
ok = false; /* missing '=' or missing value */
break;
}
if (sockopt_parse_value((SockOptId)id, eq + 1, &val) != 0) {
ok = false; /* bad value for an allowed option */
break;
}
if (count == capacity) {
int new_cap = capacity == 0 ? 4 : capacity * 2;
SockOptEntry* grown = realloc(entries, (size_t)new_cap * sizeof(SockOptEntry));
if (!grown) {
ok = false;
break;
}
entries = grown;
capacity = new_cap;
}
entries[count].id = (SockOptId)id;
entries[count].value = val;
count++;
}
free(copy);
if (!ok) {
free(entries);
return -1;
}
*out = entries;
*out_count = count;
return 0;
}
bool config_is_remote_dest(const char* s) {
if (s == NULL)
return false;
@@ -406,6 +518,7 @@ void config_delete(Config* config) {
free(config->suffix);
free(config->address);
free(config->bind_address);
free(config->sockopts);
free(config->daemon_config);
free(config->compress_choice);
free(config->chmod_spec);
+32
View File
@@ -49,6 +49,24 @@ typedef struct {
int32_t to;
} IdentityMap;
/* --sockopts=OPTIONS allowlist. Only these option names are accepted; anything
* else is rejected (never silently ignored). TCP_NODELAY, SO_KEEPALIVE and
* SO_REUSEADDR are boolean options (value 0/1); SO_RCVBUF and SO_SNDBUF take a
* non-negative byte count. All are applied as int-sized setsockopt values. */
typedef enum {
SOCKOPT_TCP_NODELAY = 0,
SOCKOPT_SO_KEEPALIVE,
SOCKOPT_SO_RCVBUF,
SOCKOPT_SO_SNDBUF,
SOCKOPT_SO_REUSEADDR,
SOCKOPT_COUNT
} SockOptId;
typedef struct {
SockOptId id; /* allowlist index */
int value; /* 0/1 for booleans, byte count for SO_RCVBUF/SO_SNDBUF */
} SockOptEntry;
typedef struct Config {
char* version;
char* send_directory;
@@ -282,6 +300,13 @@ typedef struct Config {
char* bind_address;
bool ipv6;
bool ipv4;
/* --sockopts=OPTIONS (Phase 5, Wave B): strict allowlist of TCP/socket
* options applied via setsockopt after socket() and before connect()/bind().
* These are LOCAL socket concerns: they never cross the wire config frame.
* .address is the outgoing/source bind address (--address); .bind_address is
* reserved for daemon-side binding and is not wired yet. */
SockOptEntry* sockopts;
int sockopt_count;
// PR #182: Daemon/server mode
bool daemon;
@@ -408,4 +433,11 @@ int config_basis_append(Config* config, BasisDestType type, const char* path);
/* Validate a client-provided basis-dir path (relative, confined, non-empty). */
bool config_basis_path_valid(const char* path);
/* Parse and validate a --sockopts=OPTIONS comma-separated "OPT=VAL" list into a
* malloc'd array of at most *out_count entries. Returns 0 on success (the
* caller takes ownership of *out), or -1 on the first invalid option name or
* value. Pure/static-analysis friendly: performs no socket calls, so it is
* directly unit-testable. */
int config_sockopts_parse(const char* spec, SockOptEntry** out, int* out_count);
#endif
+210 -30
View File
@@ -5,6 +5,8 @@
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <openssl/ssl.h>
#include <signal.h>
#include <stdio.h>
@@ -28,47 +30,91 @@ static void sigchld_handler(int sig) {
errno = saved_errno;
}
Server* server_create(int port) {
/* Map a listen socket's address to its numeric port for logging, independent
* of whether it is an IPv4 or IPv6 sockaddr. */
static unsigned short server_address_port(const struct sockaddr_storage* addr) {
if (addr->ss_family == AF_INET6)
return ntohs(((const struct sockaddr_in6*)addr)->sin6_port);
if (addr->ss_family == AF_INET)
return ntohs(((const struct sockaddr_in*)addr)->sin_port);
return 0;
}
Server* server_create_ex(int port, const ServerBindOptions* bind_opts) {
Server* server = (Server*)malloc(sizeof(Server));
if (server == NULL) {
log_perror("Could not allocate space for Server");
return NULL;
}
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
if (file_descriptor < 0) {
log_perror("Could not create Socket!");
free(server);
return NULL;
}
server->file_descriptor = file_descriptor;
int opt = 1;
if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
log_perror("Error setting a socket option!");
close(server->file_descriptor);
/* Effective address family. preserve the historical default (IPv4 wildcard)
* when neither --address nor -4/-6 were given. */
int family = (bind_opts && bind_opts->family != AF_UNSPEC) ? bind_opts->family : AF_INET;
const char* bind_address = bind_opts ? bind_opts->bind_address : NULL;
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
struct addrinfo* result = NULL;
int err = getaddrinfo(bind_address, port_str, &hints, &result);
if (err != 0 || result == NULL) {
char* escaped = bind_address ? output_escape(bind_address, false) : NULL;
fprintf(stderr, "Could not resolve bind address %s (%s)\n", escaped ? escaped : "(wildcard)",
gai_strerror(err));
free(escaped);
free(server);
return NULL;
}
server->address.sin_family = AF_INET;
server->address.sin_addr.s_addr = INADDR_ANY;
server->address.sin_port = htons(port);
server->address_length = sizeof(server->address);
int file_descriptor = -1;
struct addrinfo* rp;
for (rp = result; rp != NULL; rp = rp->ai_next) {
file_descriptor = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (file_descriptor < 0)
continue;
int opt = 1;
if (setsockopt(file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
log_perror("Error setting a socket option!");
close(file_descriptor);
file_descriptor = -1;
continue;
}
if (bind(file_descriptor, rp->ai_addr, (socklen_t)rp->ai_addrlen) == 0) {
memset(&server->address, 0, sizeof(server->address));
memcpy(&server->address, rp->ai_addr, rp->ai_addrlen);
server->address_length = rp->ai_addrlen;
break;
}
log_perror("Could not bind server address");
close(file_descriptor);
file_descriptor = -1;
}
freeaddrinfo(result);
if (file_descriptor < 0) {
free(server);
return NULL;
}
server->file_descriptor = file_descriptor;
server->ssl_ctx = NULL;
server->max_connections = 100;
server->active_connections = 0;
if (bind(server->file_descriptor, (struct sockaddr*)&server->address, server->address_length) <
0) {
log_perror("Could not bind server");
close(server->file_descriptor);
free(server);
return NULL;
}
return server;
}
Server* server_create(int port) {
return server_create_ex(port, NULL);
}
void server_delete(Server** server) {
if (server == NULL || *server == NULL)
return;
@@ -126,7 +172,7 @@ static void plain_child_fn(int fd, void* ctx) {
}
bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
log_message(LOG_LEVEL_INFO, "Start Listening on Port: %d", ntohs(server->address.sin_port));
log_message(LOG_LEVEL_INFO, "Start Listening on Port: %d", server_address_port(&server->address));
struct plain_ctx ctx = {handler};
accept_loop(server, plain_child_fn, &ctx, "Received Connection");
return true;
@@ -134,7 +180,8 @@ bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
const char* log_fmt) {
log_message(LOG_LEVEL_INFO, "Start TLS Listening on Port: %d", ntohs(server->address.sin_port));
log_message(LOG_LEVEL_INFO, "Start TLS Listening on Port: %d",
server_address_port(&server->address));
accept_loop(server, child_fn, child_ctx, log_fmt);
}
@@ -178,11 +225,100 @@ Client* client_create() {
return client;
}
bool tcp_connect_socket(Client* client, char* host, int port) {
int tcp_connect_family(bool ipv4, bool ipv6) {
if (ipv4)
return AF_INET;
if (ipv6)
return AF_INET6;
return AF_UNSPEC;
}
/* The socket-option apply layer maps an allowlist SockOptId to the concrete
* level/optname pair and applies it with the correct (int) value type. The
* allowlist bounds what can ever reach this point, so the id-to-name mapping
* is total for every SOCKOPT_* value. */
static int tcp_sockopt_level(SockOptId id) {
return id == SOCKOPT_TCP_NODELAY ? IPPROTO_TCP : SOL_SOCKET;
}
static int tcp_sockopt_name(SockOptId id) {
switch (id) {
case SOCKOPT_TCP_NODELAY:
return TCP_NODELAY;
case SOCKOPT_SO_KEEPALIVE:
return SO_KEEPALIVE;
case SOCKOPT_SO_RCVBUF:
return SO_RCVBUF;
case SOCKOPT_SO_SNDBUF:
return SO_SNDBUF;
case SOCKOPT_SO_REUSEADDR:
return SO_REUSEADDR;
default:
return -1;
}
}
static bool tcp_apply_sockopts(int fd, const SockOptEntry* sockopts, int sockopt_count) {
for (int i = 0; i < sockopt_count; i++) {
int name = tcp_sockopt_name(sockopts[i].id);
if (name < 0) { /* unreachable for a validated allowlist, but stay defensive */
log_message(LOG_LEVEL_ERROR, "Unsupported socket option requested");
return false;
}
int value = sockopts[i].value;
if (setsockopt(fd, tcp_sockopt_level(sockopts[i].id), name, &value, sizeof(value)) != 0) {
log_perror("Could not apply socket option");
return false;
}
}
return true;
}
/* Resolve an explicit --address source/bind address into a sockaddr once, so
* the per-candidate connect loop can bind() the outgoing socket to it. The
* family follows the same -4/-6 hints as the destination resolution, so a
* forced family selects a matching local address; returns 0 on success. */
static int resolve_bind_address(const char* addr, int family, struct sockaddr_storage* out,
socklen_t* out_len, int* out_family) {
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family; /* AF_UNSPEC when no -4/-6 */
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
struct addrinfo* result = NULL;
int err = getaddrinfo(addr, NULL, &hints, &result);
if (err != 0 || result == NULL) {
char* escaped = output_escape(addr, false);
fprintf(stderr, "Could not resolve --address %s (%s)\n",
escaped ? escaped : "<allocation failed>", gai_strerror(err));
free(escaped);
return -1;
}
struct addrinfo* rp;
bool found = false;
for (rp = result; rp != NULL; rp = rp->ai_next) {
if (family != AF_UNSPEC && rp->ai_family != family)
continue;
memcpy(out, rp->ai_addr, rp->ai_addrlen);
*out_len = (socklen_t)rp->ai_addrlen;
*out_family = rp->ai_family;
found = true;
break;
}
freeaddrinfo(result);
return found ? 0 : -1;
}
bool tcp_connect_socket_ex(Client* client, const char* host, int port,
const TcpConnectOptions* opts) {
struct addrinfo hints;
struct addrinfo* result;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
/* TcpConnectOptions.family already encodes -4/-6 (or AF_UNSPEC); feed it
* straight into the getaddrinfo hints so the destination resolution is
* (optionally) pinned to one address family. */
hints.ai_family = opts ? opts->family : AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
@@ -198,6 +334,18 @@ bool tcp_connect_socket(Client* client, char* host, int port) {
return false;
}
/* Resolve the optional --address source address once up front. */
struct sockaddr_storage bind_addr;
socklen_t bind_addr_len = 0;
int bind_addr_family = 0;
if (opts && opts->bind_address) {
if (resolve_bind_address(opts->bind_address, hints.ai_family, &bind_addr, &bind_addr_len,
&bind_addr_family) != 0) {
freeaddrinfo(result);
return false;
}
}
struct addrinfo* rp;
bool connected = false;
for (rp = result; rp != NULL; rp = rp->ai_next) {
@@ -208,12 +356,33 @@ bool tcp_connect_socket(Client* client, char* host, int port) {
if (client->file_descriptor < 0)
continue;
if (opts && opts->sockopt_count > 0 &&
!tcp_apply_sockopts(client->file_descriptor, opts->sockopts, opts->sockopt_count)) {
close(client->file_descriptor);
client->file_descriptor = -1;
break;
}
struct timeval ct;
ct.tv_sec = g_contimeout_sec;
ct.tv_usec = 0;
setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct));
setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct));
if (bind_addr_family != 0) {
if (rp->ai_family != bind_addr_family) {
close(client->file_descriptor);
client->file_descriptor = -1;
continue;
}
if (bind(client->file_descriptor, (struct sockaddr*)&bind_addr, bind_addr_len) != 0) {
log_perror("Could not bind outgoing socket to --address");
close(client->file_descriptor);
client->file_descriptor = -1;
break;
}
}
memcpy(&client->address, rp->ai_addr, rp->ai_addrlen);
client->address_length = rp->ai_addrlen;
@@ -233,8 +402,19 @@ bool tcp_connect_socket(Client* client, char* host, int port) {
return true;
}
bool client_connect(Client* client, char* host, int port) {
if (!tcp_connect_socket(client, host, port))
bool tcp_connect_socket(Client* client, const char* host, int port) {
return tcp_connect_socket_ex(client, host, port, NULL);
}
bool client_connect_ex(Client* client, const char* host, int port, const TcpConnectOptions* opts) {
if (!tcp_connect_socket_ex(client, host, port, opts))
return false;
tcp_apply_socket_timeout(client->file_descriptor);
return true;
}
bool client_connect(Client* client, const char* host, int port) {
if (!tcp_connect_socket_ex(client, host, port, NULL))
return false;
tcp_apply_socket_timeout(client->file_descriptor);
return true;
+33 -3
View File
@@ -1,12 +1,14 @@
#ifndef TRANSPORT_TCP_H
#define TRANSPORT_TCP_H
#include "config.h"
#include <netdb.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <sys/types.h>
typedef struct Server {
struct sockaddr_in address;
struct sockaddr_storage address;
unsigned int address_length;
int file_descriptor;
void* ssl_ctx;
@@ -23,18 +25,46 @@ typedef struct Client {
void* ssl_ctx;
} Client;
/* Options controlling the server's listening bind (/--address, -4/-6). When
* bind_address is NULL and family is AF_UNSPEC the existing default is used:
* an IPv4 wildcard (INADDR_ANY). */
typedef struct {
const char* bind_address; /* explicit address to bind, or NULL for wildcard */
int family; /* AF_INET / AF_INET6, or AF_UNSPEC to use the default */
} ServerBindOptions;
/* Options controlling an outgoing client connect (--address, -4/-6,
* --sockopts). All fields are client/connection-level and never cross the
* wire config frame. */
typedef struct {
const char* bind_address; /* --address: local source address to bind, or NULL */
int family; /* AF_INET / AF_INET6 / AF_UNSPEC (from -4 / -6) */
const SockOptEntry* sockopts; /* --sockopts allowlist entries */
int sockopt_count;
} TcpConnectOptions;
Server* server_create_ex(int port, const ServerBindOptions* bind_opts);
Server* server_create(int port);
bool server_listen(Server* server, void (*handler)(int file_descriptor));
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
const char* log_fmt);
void server_delete(Server** server);
Client* client_create();
bool client_connect(Client* client, char* host, int port);
bool tcp_connect_socket(Client* client, char* host, int port);
bool client_connect_ex(Client* client, const char* host, int port, const TcpConnectOptions* opts);
bool client_connect(Client* client, const char* host, int port);
bool tcp_connect_socket_ex(Client* client, const char* host, int port,
const TcpConnectOptions* opts);
bool tcp_connect_socket(Client* client, const char* host, int port);
void client_disconnect(Client* client);
void client_delete(Client* client);
void tcp_set_timeouts(int timeout_sec, int contimeout_sec);
int tcp_get_contimeout_sec(void);
int tcp_get_timeout_sec(void);
/* Resolve -4/-6 flags to a getaddrinfo ai_family value. ipv4 wins over ipv6;
* when neither is set it returns AF_UNSPEC. 0 means "no preference" and is
* therefore never returned; callers that need the "no explicit flag" sentinel
* compare the flags directly. */
int tcp_connect_family(bool ipv4, bool ipv6);
#endif
+9 -3
View File
@@ -186,9 +186,10 @@ bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
return true;
}
bool client_connect_tls(Client* client, char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path) {
if (!tcp_connect_socket(client, host, port)) {
bool client_connect_tls_ex(Client* client, const char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path,
const TcpConnectOptions* opts) {
if (!tcp_connect_socket_ex(client, host, port, opts)) {
if (client->file_descriptor >= 0)
close(client->file_descriptor);
client->file_descriptor = -1;
@@ -219,3 +220,8 @@ bool client_connect_tls(Client* client, char* host, int port, const char* cert_p
io_set_ssl(ssl);
return true;
}
bool client_connect_tls(Client* client, const char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path) {
return client_connect_tls_ex(client, host, port, cert_path, key_path, ca_path, NULL);
}
+4 -1
View File
@@ -9,7 +9,10 @@ bool tls_global_init(void);
bool server_create_tls(Server* server, const char* cert_path, const char* key_path,
const char* ca_path);
bool server_listen_tls(Server* server, void (*handler)(int file_descriptor));
bool client_connect_tls(Client* client, char* host, int port, const char* cert_path,
bool client_connect_tls_ex(Client* client, const char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path,
const TcpConnectOptions* opts);
bool client_connect_tls(Client* client, const char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path);
#endif