feat(daemon): host ACL, configurable max connections, peer audit, auth-failure delay
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
#include "daemon_conf.h"
|
||||
#include "credentials.h"
|
||||
#include "utils.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -49,6 +53,144 @@ static bool parse_bool_value(const char* value, bool* out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Parse an IPv4/IPv6 CIDR "addr/prefix" into `bytes`/`*family`. Returns false
|
||||
* for a malformed address, a missing/oversized prefix, or a prefix that does
|
||||
* not fit the address family. */
|
||||
static bool parse_cidr(const char* cidr, int* prefix_out, uint8_t* bytes, int* family_out) {
|
||||
const char* slash = strchr(cidr, '/');
|
||||
if (!slash)
|
||||
return false;
|
||||
size_t addr_len = (size_t)(slash - cidr);
|
||||
if (addr_len == 0 || addr_len >= INET6_ADDRSTRLEN)
|
||||
return false;
|
||||
char addr[INET6_ADDRSTRLEN];
|
||||
memcpy(addr, cidr, addr_len);
|
||||
addr[addr_len] = '\0';
|
||||
char* end = NULL;
|
||||
long prefix = strtol(slash + 1, &end, 10);
|
||||
if (end == slash + 1 || *end != '\0')
|
||||
return false;
|
||||
struct in_addr v4;
|
||||
struct in6_addr v6;
|
||||
if (inet_pton(AF_INET, addr, &v4) == 1) {
|
||||
if (prefix < 0 || prefix > 32)
|
||||
return false;
|
||||
memcpy(bytes, &v4, sizeof(v4));
|
||||
*prefix_out = (int)prefix;
|
||||
*family_out = AF_INET;
|
||||
return true;
|
||||
}
|
||||
if (inet_pton(AF_INET6, addr, &v6) == 1) {
|
||||
if (prefix < 0 || prefix > 128)
|
||||
return false;
|
||||
memcpy(bytes, &v6, sizeof(v6));
|
||||
*prefix_out = (int)prefix;
|
||||
*family_out = AF_INET6;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A host pattern is valid when it is non-empty and, when it contains a '/', its
|
||||
* address/prefix halves parse as a CIDR. Literals, `*` and globs are accepted
|
||||
* as-is (a glob only ever matches a peer of the same shape). */
|
||||
static bool host_pattern_valid(const char* pattern) {
|
||||
if (!pattern || *pattern == '\0')
|
||||
return false;
|
||||
if (!strchr(pattern, '/'))
|
||||
return true;
|
||||
uint8_t bytes[16];
|
||||
int prefix;
|
||||
int family;
|
||||
return parse_cidr(pattern, &prefix, bytes, &family);
|
||||
}
|
||||
|
||||
/* Append every comma- and/or whitespace-separated host pattern in `value` to
|
||||
* the heap-owned list. Returns false (err filled) on an invalid pattern or an
|
||||
* allocation failure. */
|
||||
static bool store_host_list(char*** list, int* count, const char* value, const char* key,
|
||||
const char* module_name, char* err, size_t err_size) {
|
||||
char* copy = str_dup(value);
|
||||
if (!copy) {
|
||||
if (module_name)
|
||||
set_error(err, err_size, "out of memory parsing '%s' for module '%s'", key, module_name);
|
||||
else
|
||||
set_error(err, err_size, "out of memory parsing '%s'", key);
|
||||
return false;
|
||||
}
|
||||
char* save = NULL;
|
||||
for (char* token = strtok_r(copy, ", \t", &save); token; token = strtok_r(NULL, ", \t", &save)) {
|
||||
if (!host_pattern_valid(token)) {
|
||||
if (module_name)
|
||||
set_error(err, err_size, "module '%s': invalid host pattern '%s' in '%s'", module_name,
|
||||
token, key);
|
||||
else
|
||||
set_error(err, err_size, "invalid host pattern '%s' in '%s'", token, key);
|
||||
free(copy);
|
||||
return false;
|
||||
}
|
||||
char** grown = realloc(*list, (size_t)(*count + 1) * sizeof(char*));
|
||||
if (!grown) {
|
||||
if (module_name)
|
||||
set_error(err, err_size, "out of memory parsing '%s' for module '%s'", key, module_name);
|
||||
else
|
||||
set_error(err, err_size, "out of memory parsing '%s'", key);
|
||||
free(copy);
|
||||
return false;
|
||||
}
|
||||
*list = grown;
|
||||
char* dup = str_dup(token);
|
||||
if (!dup) {
|
||||
if (module_name)
|
||||
set_error(err, err_size, "out of memory parsing '%s' for module '%s'", key, module_name);
|
||||
else
|
||||
set_error(err, err_size, "out of memory parsing '%s'", key);
|
||||
free(copy);
|
||||
return false;
|
||||
}
|
||||
(*list)[(*count)++] = dup;
|
||||
}
|
||||
free(copy);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Parse a `max connections` value: a positive integer (0/negative/garbage are
|
||||
* rejected because they would silently disable the cap or admit nothing). */
|
||||
static bool store_max_connections(int* slot, const char* value, const char* module_name, char* err,
|
||||
size_t err_size) {
|
||||
char* end = NULL;
|
||||
errno = 0;
|
||||
long n = strtol(value, &end, 10);
|
||||
if (*value == '\0' || errno != 0 || *end != '\0' || n <= 0 || n > INT_MAX) {
|
||||
if (module_name)
|
||||
set_error(err, err_size,
|
||||
"module '%s': invalid 'max connections' '%s' (must be a positive "
|
||||
"integer)",
|
||||
module_name, value);
|
||||
else
|
||||
set_error(err, err_size, "invalid 'max connections' '%s' (must be a positive integer)",
|
||||
value);
|
||||
return false;
|
||||
}
|
||||
*slot = (int)n;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Parse an `auth failure delay` value: 0 (disabled) through the configured cap. */
|
||||
static bool store_auth_failure_delay(int* slot, const char* value, char* err, size_t err_size) {
|
||||
char* end = NULL;
|
||||
errno = 0;
|
||||
long n = strtol(value, &end, 10);
|
||||
if (*value == '\0' || errno != 0 || *end != '\0' || n < 0 ||
|
||||
n > DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS) {
|
||||
set_error(err, err_size, "invalid 'auth failure delay' '%s' (must be 0-%d milliseconds)", value,
|
||||
DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS);
|
||||
return false;
|
||||
}
|
||||
*slot = (int)n;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool daemon_module_name_valid(const char* name) {
|
||||
if (!name || *name == '\0')
|
||||
return false;
|
||||
@@ -69,14 +211,25 @@ DaemonConf* daemon_conf_create(void) {
|
||||
if (!conf)
|
||||
return NULL;
|
||||
conf->global.port = DAEMON_CONF_DEFAULT_PORT;
|
||||
conf->global.max_connections = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS;
|
||||
conf->global.auth_failure_delay_ms = DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS;
|
||||
return conf;
|
||||
}
|
||||
|
||||
/* Free a heap-owned pattern list of `count` entries. */
|
||||
static void free_string_list(char** list, int count) {
|
||||
for (int i = 0; i < count; i++)
|
||||
free(list[i]);
|
||||
free(list);
|
||||
}
|
||||
|
||||
void daemon_conf_free(DaemonConf* conf) {
|
||||
if (!conf)
|
||||
return;
|
||||
free(conf->global.motd_file);
|
||||
free(conf->global.address);
|
||||
free_string_list(conf->global.hosts_allow, conf->global.hosts_allow_count);
|
||||
free_string_list(conf->global.hosts_deny, conf->global.hosts_deny_count);
|
||||
for (int i = 0; i < conf->module_count; i++) {
|
||||
DaemonModule* m = &conf->modules[i];
|
||||
free(m->name);
|
||||
@@ -84,6 +237,8 @@ void daemon_conf_free(DaemonConf* conf) {
|
||||
for (int j = 0; j < m->auth_user_count; j++)
|
||||
free(m->auth_users[j]);
|
||||
free(m->auth_users);
|
||||
free_string_list(m->hosts_allow, m->hosts_allow_count);
|
||||
free_string_list(m->hosts_deny, m->hosts_deny_count);
|
||||
}
|
||||
free(conf->modules);
|
||||
free(conf);
|
||||
@@ -141,6 +296,16 @@ static bool apply_global_key(DaemonConf* conf, char* key, const char* value, cha
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (key_equals(key, "max connections"))
|
||||
return store_max_connections(&conf->global.max_connections, value, NULL, err, err_size);
|
||||
if (key_equals(key, "auth failure delay"))
|
||||
return store_auth_failure_delay(&conf->global.auth_failure_delay_ms, value, err, err_size);
|
||||
if (key_equals(key, "hosts allow"))
|
||||
return store_host_list(&conf->global.hosts_allow, &conf->global.hosts_allow_count, value,
|
||||
"hosts allow", NULL, err, err_size);
|
||||
if (key_equals(key, "hosts deny"))
|
||||
return store_host_list(&conf->global.hosts_deny, &conf->global.hosts_deny_count, value,
|
||||
"hosts deny", NULL, err, err_size);
|
||||
set_error(err, err_size, "unknown global key '%s'", key);
|
||||
return false;
|
||||
}
|
||||
@@ -220,6 +385,14 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
||||
free(list);
|
||||
return true;
|
||||
}
|
||||
if (key_equals(key, "max connections"))
|
||||
return store_max_connections(&module->max_connections, value, module->name, err, err_size);
|
||||
if (key_equals(key, "hosts allow"))
|
||||
return store_host_list(&module->hosts_allow, &module->hosts_allow_count, value, "hosts allow",
|
||||
module->name, err, err_size);
|
||||
if (key_equals(key, "hosts deny"))
|
||||
return store_host_list(&module->hosts_deny, &module->hosts_deny_count, value, "hosts deny",
|
||||
module->name, err, err_size);
|
||||
set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name);
|
||||
return false;
|
||||
}
|
||||
@@ -459,3 +632,86 @@ int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err
|
||||
free(copy);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Compare the first `prefix` bits of two 16-byte address buffers. */
|
||||
static bool bit_prefix_match(const uint8_t* a, const uint8_t* b, int prefix) {
|
||||
int whole = prefix / 8;
|
||||
if (whole > 0 && memcmp(a, b, (size_t)whole) != 0)
|
||||
return false;
|
||||
int remainder = prefix % 8;
|
||||
if (remainder == 0)
|
||||
return true;
|
||||
uint8_t mask = (uint8_t)(0xffu << (8 - remainder));
|
||||
return (a[whole] & mask) == (b[whole] & mask);
|
||||
}
|
||||
|
||||
/* Case-insensitive glob match used for hostname patterns. Falls back to the
|
||||
* shared case-sensitive matcher when an operand is too long for the stack
|
||||
* buffers. */
|
||||
static bool host_glob_match(const char* pattern, const char* str) {
|
||||
char pbuf[256];
|
||||
char sbuf[256];
|
||||
size_t plen = strlen(pattern);
|
||||
size_t slen = strlen(str);
|
||||
if (plen >= sizeof(pbuf) || slen >= sizeof(sbuf))
|
||||
return glob_match(pattern, str);
|
||||
for (size_t i = 0; i <= plen; i++)
|
||||
pbuf[i] = (char)tolower((unsigned char)pattern[i]);
|
||||
for (size_t i = 0; i <= slen; i++)
|
||||
sbuf[i] = (char)tolower((unsigned char)str[i]);
|
||||
return glob_match(pbuf, sbuf);
|
||||
}
|
||||
|
||||
bool daemon_host_pattern_match(const char* pattern, const char* peer_ip) {
|
||||
if (!pattern || *pattern == '\0' || !peer_ip || *peer_ip == '\0')
|
||||
return false;
|
||||
if (strcmp(pattern, "*") == 0)
|
||||
return true;
|
||||
if (strchr(pattern, '/')) {
|
||||
uint8_t pattern_bytes[16];
|
||||
uint8_t peer_bytes[16];
|
||||
int prefix = 0;
|
||||
int family = AF_UNSPEC;
|
||||
if (!parse_cidr(pattern, &prefix, pattern_bytes, &family))
|
||||
return false;
|
||||
if (inet_pton(family, peer_ip, peer_bytes) != 1)
|
||||
return false;
|
||||
return bit_prefix_match(pattern_bytes, peer_bytes, prefix);
|
||||
}
|
||||
struct in_addr pattern_v4;
|
||||
struct in_addr peer_v4;
|
||||
if (inet_pton(AF_INET, pattern, &pattern_v4) == 1)
|
||||
return inet_pton(AF_INET, peer_ip, &peer_v4) == 1 && pattern_v4.s_addr == peer_v4.s_addr;
|
||||
struct in6_addr pattern_v6;
|
||||
struct in6_addr peer_v6;
|
||||
if (inet_pton(AF_INET6, pattern, &pattern_v6) == 1)
|
||||
return inet_pton(AF_INET6, peer_ip, &peer_v6) == 1 &&
|
||||
memcmp(&pattern_v6, &peer_v6, sizeof(pattern_v6)) == 0;
|
||||
/* Not a literal: a hostname/glob pattern. */
|
||||
return host_glob_match(pattern, peer_ip);
|
||||
}
|
||||
|
||||
bool daemon_hosts_allowed(const char* peer_ip, char* const* allow, int allow_count,
|
||||
char* const* deny, int deny_count) {
|
||||
if (!peer_ip)
|
||||
return false;
|
||||
for (int i = 0; i < deny_count; i++) {
|
||||
if (daemon_host_pattern_match(deny[i], peer_ip))
|
||||
return false;
|
||||
}
|
||||
if (allow_count > 0) {
|
||||
for (int i = 0; i < allow_count; i++) {
|
||||
if (daemon_host_pattern_match(allow[i], peer_ip))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool daemon_hosts_restricted(char* const* allow, int allow_count, char* const* deny,
|
||||
int deny_count) {
|
||||
(void)allow;
|
||||
(void)deny;
|
||||
return allow_count > 0 || deny_count > 0;
|
||||
}
|
||||
|
||||
@@ -52,14 +52,32 @@ typedef struct DaemonModule {
|
||||
activities. Without it the daemon refuses all of them. */
|
||||
char** auth_users; /* `auth users = a,b`; Wave B credential list */
|
||||
int auth_user_count;
|
||||
/* `max connections = N` (optional per-module cap). 0 means "not set"
|
||||
* (inherit the global cap). Parsed, stored, and validated, but NOT enforced
|
||||
* per-module: connections are counted in the accept-loop parent before the
|
||||
* client's module is known, so only the global cap is enforced (see
|
||||
* transport_tcp.c and the Daemon Mode notes in RSYNC_COMPAT.md). */
|
||||
int max_connections;
|
||||
char** hosts_allow; /* `hosts allow = a,b`; host access allow patterns */
|
||||
int hosts_allow_count;
|
||||
char** hosts_deny; /* `hosts deny = a,b`; host access deny patterns */
|
||||
int hosts_deny_count;
|
||||
} DaemonModule;
|
||||
|
||||
/* Global (pre-module) scalar keys. `motd file` is parsed and stored but has
|
||||
* no wire effect yet (MOTD display is Wave C). */
|
||||
typedef struct DaemonConfGlobals {
|
||||
int port; /* `port`, default DAEMON_CONF_DEFAULT_PORT (873) */
|
||||
char* motd_file; /* `motd file`, may be NULL */
|
||||
char* address; /* `address` (optional bind address), may be NULL */
|
||||
int port; /* `port`, default DAEMON_CONF_DEFAULT_PORT (873) */
|
||||
char* motd_file; /* `motd file`, may be NULL */
|
||||
char* address; /* `address` (optional bind address), may be NULL */
|
||||
int max_connections; /* `max connections`, default
|
||||
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */
|
||||
int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default
|
||||
DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS */
|
||||
char** hosts_allow; /* `hosts allow`; global host access allow patterns */
|
||||
int hosts_allow_count;
|
||||
char** hosts_deny; /* `hosts deny`; global host access deny patterns */
|
||||
int hosts_deny_count;
|
||||
} DaemonConfGlobals;
|
||||
|
||||
typedef struct DaemonConf {
|
||||
@@ -69,6 +87,14 @@ typedef struct DaemonConf {
|
||||
} DaemonConf;
|
||||
|
||||
#define DAEMON_CONF_DEFAULT_PORT 873
|
||||
/* Default global connection cap when `max connections` is absent. Matches the
|
||||
* historical hardcoded listener value. */
|
||||
#define DAEMON_CONF_DEFAULT_MAX_CONNECTIONS 100
|
||||
/* Default `auth failure delay` in milliseconds (0 disables the throttle). */
|
||||
#define DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS 500
|
||||
/* Largest accepted `auth failure delay`, so a typo cannot pin a connection
|
||||
* child in nanosleep for an absurd time. */
|
||||
#define DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS 60000
|
||||
/* Longest accepted config line (excluding the trailing newline). Longer lines
|
||||
* are rejected rather than buffered unboundedly. */
|
||||
#define DAEMON_CONF_MAX_LINE 4096
|
||||
@@ -99,9 +125,30 @@ const DaemonModule* daemon_conf_find_module(const DaemonConf* conf, const char*
|
||||
bool daemon_module_name_valid(const char* name);
|
||||
|
||||
/* Parse one --dparam=KEY=VALUE (or "--dparam KEY=VALUE") override string and
|
||||
* apply it to the global scalars only. Keys are case-insensitive and limited
|
||||
* to the global scalar keys defined by the grammar (port, motd file, address).
|
||||
* Returns 0 on success, -1 on error (err filled). */
|
||||
* apply it to the global keys only. Keys are case-insensitive and limited to
|
||||
* the global keys defined by the grammar (port, motd file, address,
|
||||
* max connections, auth failure delay, hosts allow, hosts deny). Returns 0 on
|
||||
* success, -1 on error (err filled). */
|
||||
int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err, size_t err_size);
|
||||
|
||||
/* Host access-control matching (pure; no I/O). `daemon_host_pattern_match`
|
||||
* matches one configured pattern against a numeric peer IP string. Supported
|
||||
* patterns: `*` (match anything), an IPv4/IPv6 literal, an IPv4/IPv6 CIDR
|
||||
* (`10.0.0.0/8`, `2001:db8::/32`), or a glob (`*.example.com`) evaluated with
|
||||
* the same matcher as file globs; a glob only matches a peer string of the
|
||||
* same shape, so a numeric peer never matches a hostname glob. */
|
||||
bool daemon_host_pattern_match(const char* pattern, const char* peer_ip);
|
||||
|
||||
/* rsync-like combined decision over a deny list and an allow list: a matching
|
||||
* deny rejects (deny takes precedence); otherwise, when any allow entries
|
||||
* exist, a peer that matches none is rejected; with no allow entries every
|
||||
* peer not denied is accepted. An empty/unset pair returns true. */
|
||||
bool daemon_hosts_allowed(const char* peer_ip, char* const* allow, int allow_count,
|
||||
char* const* deny, int deny_count);
|
||||
|
||||
/* True when at least one allow or deny pattern is configured (i.e. an
|
||||
* unprovable peer must fail closed rather than being treated as unrestricted). */
|
||||
bool daemon_hosts_restricted(char* const* allow, int allow_count, char* const* deny,
|
||||
int deny_count);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -115,6 +115,11 @@ Server* server_create(int port) {
|
||||
return server_create_ex(port, NULL);
|
||||
}
|
||||
|
||||
void server_set_max_connections(Server* server, unsigned int max_connections) {
|
||||
if (server && max_connections > 0)
|
||||
server->max_connections = max_connections;
|
||||
}
|
||||
|
||||
void server_delete(Server** server) {
|
||||
if (server == NULL || *server == NULL)
|
||||
return;
|
||||
@@ -135,7 +140,7 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
||||
}
|
||||
signal(SIGCHLD, sigchld_handler);
|
||||
while (1) {
|
||||
struct sockaddr_in client_addr;
|
||||
struct sockaddr_storage client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len);
|
||||
if (fd < 0) {
|
||||
@@ -143,13 +148,16 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
||||
continue;
|
||||
}
|
||||
tcp_apply_socket_timeout(fd);
|
||||
char peer[128];
|
||||
if (!utils_sockaddr_to_string((const struct sockaddr*)&client_addr, peer, sizeof(peer)))
|
||||
snprintf(peer, sizeof(peer), "unknown");
|
||||
if ((unsigned int)g_active_connections >= server->max_connections) {
|
||||
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting",
|
||||
server->max_connections);
|
||||
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting %s",
|
||||
server->max_connections, peer);
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
log_message(LOG_LEVEL_INFO, "%s", log_fmt);
|
||||
log_message(LOG_LEVEL_INFO, "%s from %s", log_fmt, peer);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
/* Connection children must not run the parent's global cleanup(): it
|
||||
|
||||
@@ -45,6 +45,9 @@ typedef struct {
|
||||
|
||||
Server* server_create_ex(int port, const ServerBindOptions* bind_opts);
|
||||
Server* server_create(int port);
|
||||
/* Override the listener's connection cap (the global daemon `max connections`
|
||||
* value). A non-positive value is ignored so the default cap stands. */
|
||||
void server_set_max_connections(Server* server, unsigned int max_connections);
|
||||
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);
|
||||
|
||||
@@ -587,6 +587,62 @@ bool utils_fd_peer_is_local(int fd) {
|
||||
return utils_sockaddr_is_loopback((const struct sockaddr*)&peer);
|
||||
}
|
||||
|
||||
/* Numeric peer address of a connected fd. Only AF_INET/AF_INET6 peers are
|
||||
formatted; every other descriptor/family (pipe, AF_UNIX socketpair, ...) or a
|
||||
getpeername failure returns false with buf emptied. The caller must treat
|
||||
that as "cannot tell". */
|
||||
bool utils_fd_peer_ip(int fd, char* buf, size_t len) {
|
||||
if (!buf || len == 0)
|
||||
return false;
|
||||
buf[0] = '\0';
|
||||
if (fd < 0)
|
||||
return false;
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t peer_len = sizeof(peer);
|
||||
if (getpeername(fd, (struct sockaddr*)&peer, &peer_len) != 0)
|
||||
return false;
|
||||
const void* src = NULL;
|
||||
int family = peer.ss_family;
|
||||
if (family == AF_INET)
|
||||
src = &((const struct sockaddr_in*)&peer)->sin_addr;
|
||||
else if (family == AF_INET6)
|
||||
src = &((const struct sockaddr_in6*)&peer)->sin6_addr;
|
||||
else
|
||||
return false;
|
||||
return inet_ntop(family, src, buf, (socklen_t)len) != NULL;
|
||||
}
|
||||
|
||||
/* "ip:port" / "[ip]:port" for a connected peer, used to log the connecting
|
||||
address in the accept loop. Returns false for a non-INET family. */
|
||||
bool utils_sockaddr_to_string(const struct sockaddr* addr, char* buf, size_t len) {
|
||||
if (!addr || !buf || len == 0)
|
||||
return false;
|
||||
buf[0] = '\0';
|
||||
char ip[INET6_ADDRSTRLEN];
|
||||
unsigned short port;
|
||||
int written;
|
||||
if (addr->sa_family == AF_INET) {
|
||||
const struct sockaddr_in* v4 = (const struct sockaddr_in*)addr;
|
||||
if (!inet_ntop(AF_INET, &v4->sin_addr, ip, sizeof(ip)))
|
||||
return false;
|
||||
port = ntohs(v4->sin_port);
|
||||
written = snprintf(buf, len, "%s:%u", ip, port);
|
||||
} else if (addr->sa_family == AF_INET6) {
|
||||
const struct sockaddr_in6* v6 = (const struct sockaddr_in6*)addr;
|
||||
if (!inet_ntop(AF_INET6, &v6->sin6_addr, ip, sizeof(ip)))
|
||||
return false;
|
||||
port = ntohs(v6->sin6_port);
|
||||
written = snprintf(buf, len, "[%s]:%u", ip, port);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (written < 0 || (size_t)written >= len) {
|
||||
buf[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* True when a client-supplied host string names a loopback destination:
|
||||
"localhost", any 127.0.0.0/8 literal, "::1", or "[::1]". */
|
||||
bool utils_host_is_loopback(const char* host) {
|
||||
|
||||
@@ -77,5 +77,13 @@ bool append_tail_length(unsigned long long old_size, unsigned long long check_si
|
||||
bool utils_sockaddr_is_loopback(const struct sockaddr* addr);
|
||||
bool utils_fd_peer_is_local(int fd);
|
||||
bool utils_host_is_loopback(const char* host);
|
||||
/* Numeric peer address of a connected fd (INET6_ADDRSTRLEN is always enough).
|
||||
* Returns false and leaves buf empty when the fd is not a connected INET socket
|
||||
* or getpeername/inet_ntop fails. Used by the daemon host-access gate; a false
|
||||
* return is "cannot tell" and must be treated as fail-closed when ACLs apply. */
|
||||
bool utils_fd_peer_ip(int fd, char* buf, size_t len);
|
||||
/* Format a sockaddr as "ip:port" (IPv4) or "[ip]:port" (IPv6) for logging.
|
||||
* Returns false (buf emptied) for a non-INET family or a formatting failure. */
|
||||
bool utils_sockaddr_to_string(const struct sockaddr* addr, char* buf, size_t len);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user