feat(p5-socket): --address, -4/-6, --sockopts, server bind options
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user