feat(p5-socket): --address, -4/-6, --sockopts, server bind options
This commit is contained in:
+6
-4
@@ -610,12 +610,12 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
||||
| `-e`, `--rsh=COMMAND` | Remote shell to use | ❌ Not Implemented | Removed; SSH invokes `ssh` directly |
|
||||
| `--rsync-path=PROGRAM` | rsync binary on remote | ❌ Not Implemented | Removed; use `--fastsync-server-path` |
|
||||
| `--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 reset when absent. Local socket concern: never crosses the wire |
|
||||
| `--blocking-io` | Use blocking I/O for remote shell | ❌ Not Implemented | |
|
||||
| `--outbuf=N\|L\|B` | Set output buffering | ❌ Not Implemented | |
|
||||
| `--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 | Generalize SSH command construction and subprocess I/O while retaining argument escaping and timeout guarantees. |
|
||||
| `--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. |
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
@@ -530,6 +548,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)},
|
||||
@@ -1125,6 +1146,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;
|
||||
|
||||
@@ -373,12 +373,21 @@ 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);
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -166,6 +166,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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
@@ -133,6 +135,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;
|
||||
@@ -339,6 +343,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 (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);
|
||||
|
||||
@@ -40,6 +40,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;
|
||||
@@ -264,6 +282,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;
|
||||
@@ -390,4 +415,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
-29
@@ -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,8 @@ 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 +181,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 +226,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 +335,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 +357,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 +403,19 @@ bool tcp_connect_socket(Client* client, char* host, int port) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tcp_connect_socket(Client* client, 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, char* host, int port) {
|
||||
if (!tcp_connect_socket(client, host, port))
|
||||
if (!tcp_connect_socket_ex(client, host, port, NULL))
|
||||
return false;
|
||||
tcp_apply_socket_timeout(client->file_descriptor);
|
||||
return true;
|
||||
|
||||
@@ -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,13 +25,35 @@ 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_ex(Client* client, const char* host, int port, const TcpConnectOptions* opts);
|
||||
bool client_connect(Client* client, 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, char* host, int port);
|
||||
void client_disconnect(Client* client);
|
||||
void client_delete(Client* client);
|
||||
@@ -37,4 +61,10 @@ 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
|
||||
|
||||
@@ -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, 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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ 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_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, char* host, int port, const char* cert_path,
|
||||
const char* key_path, const char* ca_path);
|
||||
|
||||
|
||||
@@ -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
@@ -834,10 +834,7 @@ static void test_parse_args_rejects_unimplemented_options() {
|
||||
"-e",
|
||||
"--rsh",
|
||||
"--rsync-path",
|
||||
"--address",
|
||||
"--bind-address",
|
||||
"--ipv6",
|
||||
"--ipv4",
|
||||
"--daemon",
|
||||
"--config",
|
||||
"--server"};
|
||||
@@ -2480,6 +2477,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();
|
||||
@@ -2493,6 +2574,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();
|
||||
|
||||
+119
-1
@@ -2,14 +2,126 @@
|
||||
#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);
|
||||
if (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);
|
||||
if (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 +211,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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user