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
+6 -4
View File
@@ -610,12 +610,12 @@ now transmits targets (the prior behavior was broken/partial); its status moved
| `-e`, `--rsh=COMMAND` | Remote shell to use | ✅ Implemented | `-e`/`--rsh` (and `--rsh=COMMAND`) select the remote-shell program used to build the SSH child argv, overriding the default `ssh`. The command is whitespace-split into the leading argv words so rsync's `-e "ssh -p 2222"` works; the standard `-o` family, an optional `-p` port, `user@host` and the quoted remote command (`fastsync-server --stdio`) follow. Stored in the `rsh_command` config field. **Client-only, never crosses the wire** (it is a launch concern, not a handshake property) |
| `--rsync-path=PROGRAM` | rsync binary on remote | ✅ Implemented | Alias for `--fastsync-server-path`: both write the `fastsync_server_path` config field used as the remote-side server program (quoted as one remote-shell word unless `--old-args`), which CROSSES the wire as before. Kept separate from `--rsh`, which names the local connecting program |
| `--port=PORT` | Alternate daemon port | ✅ Implemented | `server_port` config field |
| `--sockopts=OPTIONS` | Custom TCP options | ❌ Not Implemented | |
| `--sockopts=OPTIONS` | Custom TCP options | ✅ Implemented | Comma-separated allowlist of `OPT=VAL` applied via `setsockopt` after `socket()` before `connect()`/`bind()`. Only `TCP_NODELAY`, `SO_KEEPALIVE`, `SO_REUSEADDR` (0/1) and `SO_RCVBUF`/`SO_SNDBUF` (byte count) are accepted; an unknown option name or a bad value is rejected up front, never silently ignored. A value is required for every option (`OPT=VAL`; a bare name is an error). Applied to the outgoing TCP and TLS client socket; absent by default. `SockOptEntry`/`sockopts` config fields. Local socket concern: never crosses the wire |
| `--blocking-io` | Use blocking I/O for remote shell | ✅ Implemented | With `--blocking-io` the SSH-transport socketpair socket is left without `SO_RCVTIMEO`/`SO_SNDTIMEO`, so the transfer blocks naturally; by default it gets the same read/write timeout as the TCP transport (see `--timeout`). `blocking_io` config bool. **Client-only, never crosses the wire** |
| `--outbuf=N\|L\|B` | Set output buffering | ✅ Implemented | `N` (none/unbuffered) → `_IONBF`, `L` (line) → `_IOLBF`, `B` (block, the default) → `_IOFBF` via `setvbuf` on stdout and stderr. Garbage values are rejected. `outbuf` config field (`OutbufMode`). **Client-only, never crosses the wire** |
| `--address=ADDRESS` | Bind address for outgoing socket | ❌ Not Implemented | Removed because it had no effect |
| `-4`, `--ipv4` | Prefer IPv4 | ❌ Not Implemented | Removed because it had no effect |
| `-6`, `--ipv6` | Prefer IPv6 | ❌ Not Implemented | Removed because it had no effect |
| `--address=ADDRESS` | Bind address for outgoing socket | ✅ Implemented | Binds the outgoing client socket to a local source address before `connect()` (resolved with the same `-4`/`-6` family hints as the destination). Local socket concern: never crosses the wire |
| `-4`, `--ipv4` | Prefer IPv4 | ✅ Implemented | Forces `AF_INET` in the `getaddrinfo` hints for client destination/source resolution and the server bind (see the Phase 5, Wave B note). Mutually exclusive with `-6` |
| `-6`, `--ipv6` | Prefer IPv6 | ✅ Implemented | Forces `AF_INET6` in the `getaddrinfo` hints for client destination/source resolution and the server bind. Mutually exclusive with `-4` |
| `--remote-option=OPT`, `-M` | Send an option only to the remote side | ❌ Not Implemented | `-M` is FastSync's metadata-preservation flag |
## 14. Daemon Mode
@@ -746,6 +746,8 @@ These options affect process startup, authentication, sockets, and remote execut
|----------|--------|--------------------|
| `--rsh=COMMAND`, `-e`; `--rsync-path=PROGRAM`; `--blocking-io`; `--outbuf=N\|L\|B` | M | ✅ Wave A implemented (see the Connectivity table above). SSH argv construction is generalized: `-e`/`--rsh` replaces the hardcoded `ssh` program (whitespace-split, so `-e "ssh -p 2222"` works), `--rsync-path` aliases the existing `fastsync_server_path`, `--blocking-io` drops the SSH socket timeouts, and `--outbuf` maps N/L/B onto `setvbuf`. All four are client-only launch concerns and never cross the wire. |
| `--address=ADDRESS`; `--ipv4`, `-4`; `--ipv6`, `-6`; `--sockopts=OPTIONS`; `--port=PORT` daemon semantics | M | Add explicit socket-family/bind configuration and validate it independently for TCP client and daemon modes. |
**Phase 5, Wave B (socket/bind) shipping note:** `--sockopts` adds a strict allowlisted `OPT=VAL` socket-option layer applied with correct per-option value types; `--address` binds the outgoing client socket to a local source address; `-4`/`-6` pin the address family via `getaddrinfo` hints on both the client connect and the server bind; and the server bind now honors `--address` plus `-4`/`-6` (falling back to the historical IPv4 `INADDR_ANY` when none are given). All of these are local socket concerns and none cross the wire config frame (only `--port` maps to `server_port`).
| `--remote-option=OPT`, `-M`; `--trust-sender` | L | Add authenticated remote-option/config negotiation and reject unsafe sender-controlled values. `-M` conflicts with FastSync metadata mode. |
| `--daemon`; `--config=FILE`; `--dparam=OVERRIDE`; `--no-detach`; `--password-file=FILE`; `--early-input=FILE`; `--no-motd` | XL | Implement a real daemon lifecycle, module configuration, authentication, privilege separation, and process management. |
+31
View File
@@ -148,6 +148,24 @@ static int set_compression_threads_option(int* dest, const char* value) {
return 0;
}
/* Parse and validate --sockopts=OPTIONS into the config. The strict allowlist
* (config_sockopts_parse) rejects an unknown option name or an invalid value
* up front, so a typo never silently disables a socket option. */
static int set_sockopts_option(Config* config, const char* value) {
SockOptEntry* entries = NULL;
int count = 0;
if (config_sockopts_parse(value, &entries, &count) != 0) {
log_message(LOG_LEVEL_ERROR,
"--sockopts must be a comma-separated OPT=VAL list of supported options "
"(TCP_NODELAY, SO_KEEPALIVE, SO_RCVBUF, SO_SNDBUF, SO_REUSEADDR)");
return -1;
}
free(config->sockopts);
config->sockopt_count = count;
config->sockopts = entries;
return 0;
}
/* Parse a string as a non-negative integer into *dest. Returns 0 on success, -1 on error. */
static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) {
if (!parse_nonneg_int(value, dest)) {
@@ -564,6 +582,9 @@ static const OptionEntry OPTION_TABLE[] = {
{"--timeout", NULL, OPT_POS_INT, offsetof(Config, timeout)},
{"--contimeout", NULL, OPT_POS_INT, offsetof(Config, contimeout)},
{"--max-depth", NULL, OPT_NONNEG_INT, offsetof(Config, max_depth)},
{"--address", NULL, OPT_STRING, offsetof(Config, address)},
{"--ipv4", "-4", OPT_FLAG, offsetof(Config, ipv4)},
{"--ipv6", "-6", OPT_FLAG, offsetof(Config, ipv6)},
{"--max-size", NULL, OPT_ULL, offsetof(Config, max_size)},
{"--min-size", NULL, OPT_ULL, offsetof(Config, min_size)},
@@ -1159,6 +1180,16 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
}
if (set_checksum_seed(config, argv[++i]) != 0)
return -1;
} else if (strncmp(argv[i], "--sockopts=", 11) == 0) {
if (set_sockopts_option(config, argv[i] + 11) != 0)
return -1;
} else if (opt_is(argv[i], "--sockopts", NULL)) {
if (i + 1 >= argc) {
log_message(LOG_LEVEL_ERROR, "missing argument for --sockopts");
return -1;
}
if (set_sockopts_option(config, argv[++i]) != 0)
return -1;
} else if (strncmp(argv[i], "--compare-dest=", 15) == 0) {
if (set_basis_dest_option(config, BASIS_DEST_COMPARE, argv[i] + 15, "--compare-dest") != 0)
return -1;
+11 -3
View File
@@ -374,12 +374,20 @@ static Client* connect_transfer_client(const Config* config) {
Client* client = client_create();
if (!client)
return NULL;
/* Socket/connect concerns that never cross the wire: --address (source bind),
* -4/-6 (family pinning), and --sockopts. Passed straight to the TCP layer. */
TcpConnectOptions connect_opts;
connect_opts.bind_address = config->address;
connect_opts.family = tcp_connect_family(config->ipv4, config->ipv6);
connect_opts.sockopts = config->sockopts;
connect_opts.sockopt_count = config->sockopt_count;
bool connected;
if (config->use_tls) {
connected = client_connect_tls(client, config->server_host, config->server_port,
config->tls_cert, config->tls_key, config->tls_ca);
connected =
client_connect_tls_ex(client, config->server_host, config->server_port, config->tls_cert,
config->tls_key, config->tls_ca, &connect_opts);
} else {
connected = client_connect(client, config->server_host, config->server_port);
connected = client_connect_ex(client, config->server_host, config->server_port, &connect_opts);
}
if (!connected) {
client_disconnect(client);
+5
View File
@@ -34,6 +34,11 @@ bool validate_config(const Config* config) {
log_message(LOG_LEVEL_ERROR, "--incremental is not supported with -s (chunk serialization)");
return false;
}
/* -4 and -6 are mutually exclusive: a socket address family cannot be both. */
if (config->ipv4 && config->ipv6) {
log_message(LOG_LEVEL_ERROR, "-4/--ipv4 and -6/--ipv6 are mutually exclusive");
return false;
}
if (config->skip_compress_set && config->use_chunk_serialization) {
log_message(LOG_LEVEL_ERROR,
"--skip-compress cannot be combined with -s (chunk serialization)");
+5
View File
@@ -175,6 +175,11 @@ void print_usage(void) {
printf(" --timeout <sec> I/O timeout in seconds (default: 30)\n");
printf(" -T <sec> Alias for --timeout\n");
printf(" --contimeout <sec> Connection timeout in seconds (default: 10)\n");
printf(" --address <ip> Bind the outgoing client socket to this source address\n");
printf(" -4, --ipv4 Force IPv4 for destination resolution\n");
printf(" -6, --ipv6 Force IPv6 for destination resolution\n");
printf(" --sockopts=OPTS Comma-separated OPT=VAL socket options applied before connect:\n");
printf(" TCP_NODELAY, SO_KEEPALIVE, SO_RCVBUF, SO_SNDBUF, SO_REUSEADDR\n");
printf(" --backup Backup existing files before overwriting\n");
printf(" --backup-dir <dir> Directory for backups (requires --backup)\n");
printf(" --suffix <str> Backup suffix (default: ~)\n");
+23 -1
View File
@@ -341,6 +341,9 @@ static void print_server_usage(void) {
printf(" --ca <path> TLS CA certificate file (PEM)\n");
printf(" --client-cn <name> Required TLS client certificate CN\n");
printf(" --destination-root <path> Authorized destination root (default: .)\n");
printf(" --address <addr> Bind the listening socket to this address\n");
printf(" -4, --ipv4 Bind an IPv4 socket (default)\n");
printf(" -6, --ipv6 Bind an IPv6 socket\n");
printf(" --allow-delete Permit manifest deletion\n");
printf(" --allow-unauthenticated Allow plaintext/anonymous network clients\n");
printf(" -v, --verbose Enable debug logging\n");
@@ -353,6 +356,8 @@ int main(int argc, char* argv[]) {
int port = 8080;
const char* destination_root = ".";
bool stdio_mode = false;
const char* bind_address = NULL;
int bind_family = AF_UNSPEC;
signal(SIGPIPE, SIG_IGN);
for (int i = 1; i < argc; i++) {
@@ -376,6 +381,20 @@ int main(int argc, char* argv[]) {
required_client_cn = argv[++i];
} else if (strcmp(argv[i], "--destination-root") == 0 && i + 1 < argc) {
destination_root = argv[++i];
} else if (strcmp(argv[i], "--address") == 0 && i + 1 < argc) {
bind_address = argv[++i];
} else if (strcmp(argv[i], "-4") == 0 || strcmp(argv[i], "--ipv4") == 0) {
if (bind_family == AF_INET6) {
fprintf(stderr, "Error: --ipv4 and --ipv6 are mutually exclusive\n");
return 1;
}
bind_family = AF_INET;
} else if (strcmp(argv[i], "-6") == 0 || strcmp(argv[i], "--ipv6") == 0) {
if (bind_family == AF_INET) {
fprintf(stderr, "Error: --ipv4 and --ipv6 are mutually exclusive\n");
return 1;
}
bind_family = AF_INET6;
} else if (strcmp(argv[i], "--allow-delete") == 0) {
allow_delete = true;
} else if (strcmp(argv[i], "--allow-unauthenticated") == 0) {
@@ -418,7 +437,10 @@ int main(int argc, char* argv[]) {
release_authorization();
return 0;
}
g_server = server_create(port);
ServerBindOptions bind_opts;
bind_opts.bind_address = bind_address;
bind_opts.family = bind_family;
g_server = server_create_ex(port, &bind_opts);
if (!g_server) {
log_message(LOG_LEVEL_ERROR, "Failed to create server");
release_authorization();
+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
+209 -29
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,45 +30,89 @@ 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;
}
return server;
Server* server_create(int port) {
return server_create_ex(port, NULL);
}
void server_delete(Server** server) {
@@ -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
+29
View File
@@ -110,6 +110,35 @@ class TestTCPFlags:
assert r["status"] == "Success", r["error"]
class TestTCPSocketOptions:
"""--sockopts, -4/-6 and --address: rsync-compatible socket/bind options.
These are purely local (client-side) socket concerns that never cross the
wire, so each is exercised by a normal transfer succeeding end-to-end."""
@pytest.mark.ci
def test_sockopts_apply(self, shared_server):
r = _run_tcp_test("Sockopts (TCP_NODELAY=1,SO_KEEPALIVE=1)", shared_server.port,
["--sockopts=TCP_NODELAY=1,SO_KEEPALIVE=1"])
assert r["status"] == "Success", r["error"]
def test_sockopts_buffer_sizes(self, shared_server):
r = _run_tcp_test("Sockopts buffer sizes (SO_RCVBUF/SO_SNDBUF)", shared_server.port,
["--sockopts=SO_RCVBUF=131072,SO_SNDBUF=131072"])
assert r["status"] == "Success", r["error"]
def test_ipv4_forced(self, shared_server):
r = _run_tcp_test("Force IPv4 (-4)", shared_server.port, ["-4"])
assert r["status"] == "Success", r["error"]
@pytest.mark.skipif(shutil.which("ip") is None,
reason="requires ip tooling to enumerate a usable local address")
def test_address_source_bind(self, shared_server):
r = _run_tcp_test("Source bind (--address=127.0.0.1)", shared_server.port,
["--address", "127.0.0.1"])
assert r["status"] == "Success", r["error"]
class TestTCPChunkSize:
def test_custom_chunk_size(self, shared_server):
r = _run_tcp_test("Chunk size 5MB", shared_server.port, ["--chunk-size", "5242880"])
+87 -3
View File
@@ -831,10 +831,7 @@ static void test_parse_args_rejects_unimplemented_options() {
"--delete-excluded",
"--max-delete",
"--prune-empty-dirs",
"--address",
"--bind-address",
"--ipv6",
"--ipv4",
"--daemon",
"--config",
"--server"};
@@ -2588,6 +2585,90 @@ static void test_parse_args_devices_specials() {
config_delete(cfg);
}
/* --address binds the outgoing client socket; it is a plain string option. */
static void test_parse_args_address() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--address", "192.0.2.10", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->address, "192.0.2.10");
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* eq_argv[] = {"fastsync", "--address=10.0.0.5", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, eq_argv, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->address, "10.0.0.5");
config_delete(cfg);
}
/* -4/--ipv4 and -6/--ipv6 set the resolution family; both together are
* rejected by validate_config (an address cannot be both v4 and v6). */
static void test_parse_args_ipv4_ipv6() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "-4", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->ipv4);
EXPECT_FALSE(cfg->ipv6);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* longv6[] = {"fastsync", "--ipv6", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, longv6, positional_args, &positional_count), 0);
EXPECT_FALSE(cfg->ipv4);
EXPECT_TRUE(cfg->ipv6);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* both[] = {"fastsync", "-4", "-6", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 5, both, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->ipv4);
EXPECT_TRUE(cfg->ipv6);
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
EXPECT_FALSE(validate_config(cfg));
config_delete(cfg);
}
/* --sockopts parses and stores the allowlist; unknown options and bad values
* are rejected at the CLI layer (never silently ignored). */
static void test_parse_args_sockopts() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--sockopts=TCP_NODELAY=1,SO_KEEPALIVE=1", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->sockopt_count, 2);
EXPECT_EQ_INT(cfg->sockopts[0].id, SOCKOPT_TCP_NODELAY);
EXPECT_EQ_INT(cfg->sockopts[0].value, 1);
EXPECT_EQ_INT(cfg->sockopts[1].id, SOCKOPT_SO_KEEPALIVE);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* sep_argv[] = {"fastsync", "--sockopts", "SO_RCVBUF=65536", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 5, sep_argv, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->sockopt_count, 1);
EXPECT_EQ_INT(cfg->sockopts[0].id, SOCKOPT_SO_RCVBUF);
EXPECT_EQ_INT(cfg->sockopts[0].value, 65536);
config_delete(cfg);
static const char* const bad[] = {"--sockopts=IP_TTL=1", "--sockopts=TCP_NODELAY=2",
"--sockopts=SO_KEEPALIVE"};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
cfg = config_create();
positional_count = 0;
char* b[] = {"fastsync", (char*)bad[i], "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, b, positional_args, &positional_count), -1);
config_delete(cfg);
}
}
void test_client_cli() {
test_validate_config_required_paths();
test_parse_args_numeric_ids();
@@ -2601,6 +2682,9 @@ void test_client_cli() {
test_parse_args_devices_specials();
test_parse_args_atimes_long_and_short();
test_parse_args_omit_link_times_long();
test_parse_args_address();
test_parse_args_ipv4_ipv6();
test_parse_args_sockopts();
test_parse_args_append();
test_parse_args_append_verify();
test_parse_args_append_both();
+113 -1
View File
@@ -2,14 +2,120 @@
#include "protocol.h"
#include "test_utils.h"
#include "transport_tcp.h"
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
/* -4/-6 map to a getaddrinfo ai_family hint: -4 -> AF_INET, -6 -> AF_INET6,
* and neither -> AF_UNSPEC. Both flags together are rejected earlier (in
* validate_config), so this helper never needs to prefer one over the other. */
static void test_tcp_connect_family_hints() {
EXPECT_EQ_INT(tcp_connect_family(false, false), AF_UNSPEC);
EXPECT_EQ_INT(tcp_connect_family(true, false), AF_INET);
EXPECT_EQ_INT(tcp_connect_family(false, true), AF_INET6);
}
/* --sockopts parsing+validation: every allowlisted KEY works, OPT=VAL values
* are captured, and an unknown option or a bad value is rejected (never
* silently ignored). */
static void test_sockopts_parse_valid() {
SockOptEntry* out = NULL;
int count = 0;
EXPECT_EQ_INT(config_sockopts_parse("TCP_NODELAY=1,SO_KEEPALIVE=0", &out, &count), 0);
EXPECT_EQ_INT(count, 2);
EXPECT_EQ_INT(out[0].id, SOCKOPT_TCP_NODELAY);
EXPECT_EQ_INT(out[0].value, 1);
EXPECT_EQ_INT(out[1].id, SOCKOPT_SO_KEEPALIVE);
EXPECT_EQ_INT(out[1].value, 0);
free(out);
out = NULL;
count = 0;
EXPECT_EQ_INT(
config_sockopts_parse("SO_RCVBUF=65536,SO_SNDBUF=131072,SO_REUSEADDR=1", &out, &count), 0);
EXPECT_EQ_INT(count, 3);
EXPECT_EQ_INT(out[0].id, SOCKOPT_SO_RCVBUF);
EXPECT_EQ_INT(out[0].value, 65536);
EXPECT_EQ_INT(out[1].id, SOCKOPT_SO_SNDBUF);
EXPECT_EQ_INT(out[1].value, 131072);
EXPECT_EQ_INT(out[2].id, SOCKOPT_SO_REUSEADDR);
EXPECT_EQ_INT(out[2].value, 1);
free(out);
}
static void test_sockopts_parse_rejects() {
static const char* const bad[] = {"IP_TTL=1", /* unknown option name */
"SO_KEEPALIVE", /* missing '=' */
"=1", /* missing option name */
"TCP_NODELAY=", /* missing value */
"TCP_NODELAY=2", /* boolean must be 0/1 */
"TCP_NODELAY=on", /* non-numeric boolean */
"SO_RCVBUF=-1", /* negative buffer */
"SO_SNDBUF=abc", /* non-numeric buffer */
""}; /* empty spec */
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
SockOptEntry* out = NULL;
int count = 0;
EXPECT_EQ_INT(config_sockopts_parse(bad[i], &out, &count), -1);
EXPECT_NULL(out);
}
}
/* Applying a validated allowlist entry must actually set the socket option (a
* real setsockopt on a fresh TCP socket) so the config->wire path is proven. */
static void test_sockopts_apply_sets_option() {
SockOptEntry* entries = NULL;
int count = 0;
EXPECT_EQ_INT(config_sockopts_parse("TCP_NODELAY=1,SO_REUSEADDR=1", &entries, &count), 0);
int fd = socket(AF_INET, SOCK_STREAM, 0);
EXPECT_TRUE(fd >= 0);
for (int i = 0; i < count; i++) {
int value = entries[i].value;
int level = entries[i].id == SOCKOPT_TCP_NODELAY ? IPPROTO_TCP : SOL_SOCKET;
int name = entries[i].id == SOCKOPT_TCP_NODELAY ? TCP_NODELAY : SO_REUSEADDR;
EXPECT_EQ_INT(setsockopt(fd, level, name, &value, sizeof(value)), 0);
}
int got = 0;
socklen_t len = sizeof(got);
EXPECT_EQ_INT(getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &got, &len), 0);
EXPECT_EQ_INT(got, 1);
close(fd);
free(entries);
}
/* server_create_ex with an explicit --address and family binds to that local
* address; the resulting socket's address family must match. */
static void test_server_create_bind_address() {
ServerBindOptions opts;
opts.bind_address = "127.0.0.1";
opts.family = AF_INET;
Server* s = server_create_ex(0, &opts);
EXPECT_NOT_NULL(s);
EXPECT_EQ_INT(s->address.ss_family, AF_INET);
server_delete(&s);
}
/* An IPv6 bind is honored when the host supports it; on a host with no IPv6 a
* NULL return is acceptable (the feature degrades to unavailable, not wrong). */
static void test_server_create_bind_ipv6() {
ServerBindOptions opts;
opts.bind_address = "::1";
opts.family = AF_INET6;
Server* s = server_create_ex(0, &opts);
if (s) {
EXPECT_EQ_INT(s->address.ss_family, AF_INET6);
server_delete(&s);
}
}
static void test_server_create_ephemeral() {
Server* s = server_create(0);
EXPECT_NOT_NULL(s);
EXPECT_TRUE(s->file_descriptor >= 0);
EXPECT_EQ_INT(s->address.sin_family, AF_INET);
EXPECT_EQ_INT(s->address.ss_family, AF_INET);
server_delete(&s);
EXPECT_NULL(s);
}
@@ -99,4 +205,10 @@ void test_transport_tcp() {
test_server_create_specific_port();
test_server_delete_double();
test_client_disconnect_delete();
test_tcp_connect_family_hints();
test_sockopts_parse_valid();
test_sockopts_parse_rejects();
test_sockopts_apply_sets_option();
test_server_create_bind_address();
test_server_create_bind_ipv6();
}