diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 2257ed9..726cf0a 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -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. | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index bf642c7..ea1c098 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -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; diff --git a/src/client/client_send.c b/src/client/client_send.c index da196e8..f9c7b90 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -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); diff --git a/src/client/client_validation.c b/src/client/client_validation.c index 2451f57..c0bc08a 100644 --- a/src/client/client_validation.c +++ b/src/client/client_validation.c @@ -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)"); diff --git a/src/client/usage.c b/src/client/usage.c index f23edb2..5984e51 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -175,6 +175,11 @@ void print_usage(void) { printf(" --timeout I/O timeout in seconds (default: 30)\n"); printf(" -T Alias for --timeout\n"); printf(" --contimeout Connection timeout in seconds (default: 10)\n"); + printf(" --address 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 Directory for backups (requires --backup)\n"); printf(" --suffix Backup suffix (default: ~)\n"); diff --git a/src/server/server.c b/src/server/server.c index f5bfd98..ea4b3f4 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -341,6 +341,9 @@ static void print_server_usage(void) { printf(" --ca TLS CA certificate file (PEM)\n"); printf(" --client-cn Required TLS client certificate CN\n"); printf(" --destination-root Authorized destination root (default: .)\n"); + printf(" --address 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(); diff --git a/src/shared/config.c b/src/shared/config.c index ebe785e..5cbb067 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -11,6 +11,8 @@ #include #include #include +#include +#include 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); diff --git a/src/shared/config.h b/src/shared/config.h index 698fc3e..87b4da4 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -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 diff --git a/src/shared/transport_tcp.c b/src/shared/transport_tcp.c index 953e846..b26cb8e 100644 --- a/src/shared/transport_tcp.c +++ b/src/shared/transport_tcp.c @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -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 : "", 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; diff --git a/src/shared/transport_tcp.h b/src/shared/transport_tcp.h index 156531a..b5937e6 100644 --- a/src/shared/transport_tcp.h +++ b/src/shared/transport_tcp.h @@ -1,12 +1,14 @@ #ifndef TRANSPORT_TCP_H #define TRANSPORT_TCP_H +#include "config.h" +#include #include #include #include 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 diff --git a/src/shared/transport_tls.c b/src/shared/transport_tls.c index fa21e95..c5f0df4 100644 --- a/src/shared/transport_tls.c +++ b/src/shared/transport_tls.c @@ -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); +} diff --git a/src/shared/transport_tls.h b/src/shared/transport_tls.h index b9d59e7..fd1f9d5 100644 --- a/src/shared/transport_tls.h +++ b/src/shared/transport_tls.h @@ -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 diff --git a/tests/integration/test_tcp.py b/tests/integration/test_tcp.py index 68f5fa2..c3ee210 100644 --- a/tests/integration/test_tcp.py +++ b/tests/integration/test_tcp.py @@ -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"]) diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 6d999ad..f075849 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -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(); diff --git a/tests/test_transport_tcp.c b/tests/test_transport_tcp.c index 613776e..b4b7c89 100644 --- a/tests/test_transport_tcp.c +++ b/tests/test_transport_tcp.c @@ -2,14 +2,120 @@ #include "protocol.h" #include "test_utils.h" #include "transport_tcp.h" +#include +#include #include #include +#include + +/* -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(); }