feat(client): re-resolve the server hostnames at the DNS TTL
hbc, hbc_mini.py and hbc_mini.c resolved the server once at startup, so an address change needed a client restart. They now re-resolve every TTL seconds (smallest A/AAAA TTL, floor 30s, 300s when unknown) and re-point, add or drop connections to follow the new addresses; a failed lookup keeps the current ones. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
CC ?= cc
|
||||
CFLAGS = -O2 -Wall -Wextra -std=c11
|
||||
LDFLAGS = -lz -lpthread -lm
|
||||
# res_search() lives in libresolv on older glibc; in libc on newer glibc, musl and the BSDs
|
||||
LDFLAGS += $(shell echo 'int main(void){return 0;}' | $(CC) -x c - -lresolv -o /dev/null 2>/dev/null && echo -lresolv)
|
||||
TARGET = hbc_mini
|
||||
SRC = hbc_mini.c
|
||||
|
||||
|
||||
+204
-1
@@ -20,6 +20,7 @@
|
||||
#endif
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <arpa/nameser.h>
|
||||
#include <netinet/in.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <math.h>
|
||||
#include <netdb.h>
|
||||
#include <pthread.h>
|
||||
#include <resolv.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
@@ -576,6 +578,7 @@ static int conn_send(conn_t *c, const char *msg_id, kvdict_t *d) {
|
||||
|
||||
struct sockaddr_storage sa; socklen_t salen = 0;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
pthread_mutex_lock(&c->mu); /* conn_retarget() may change addr/af/sockfd */
|
||||
if (c->af == AF_INET) {
|
||||
struct sockaddr_in *s4 = (struct sockaddr_in *)&sa;
|
||||
s4->sin_family = AF_INET;
|
||||
@@ -590,7 +593,6 @@ static int conn_send(conn_t *c, const char *msg_id, kvdict_t *d) {
|
||||
salen = sizeof(*s6);
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&c->mu);
|
||||
c->last_send = now_ts();
|
||||
ssize_t sent = sendto(c->sockfd, wire, wlen, 0, (struct sockaddr *)&sa, salen);
|
||||
pthread_mutex_unlock(&c->mu);
|
||||
@@ -1655,6 +1657,199 @@ static void usage(const char *prog) {
|
||||
" -h Help\n", prog);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Re-resolution of the server hosts at the TTL of their A/AAAA records
|
||||
* getaddrinfo() hides the TTL, so a res_search() query is made only to learn it; the
|
||||
* addresses still come from getaddrinfo(). TTL unknown (IP literal, /etc/hosts name, no
|
||||
* resolv.conf, timeout) -> DNS_DEFAULT_TTL. Runs in a detached thread so a slow resolver
|
||||
* cannot delay heartbeats; the main loop applies the result.
|
||||
* ============================================================ */
|
||||
|
||||
#define DNS_DEFAULT_TTL 300 /* used when the real TTL is unknown */
|
||||
#define DNS_MIN_TTL 30 /* a TTL of 0 must not turn into a resolution loop */
|
||||
#define DNS_FAILED_RETRY 60
|
||||
|
||||
typedef struct { int af; char addr[INET6_ADDRSTRLEN]; } dns_addr_t;
|
||||
|
||||
typedef struct {
|
||||
const char **hosts;
|
||||
int nhost, port, af_filter;
|
||||
} dns_ctx_t;
|
||||
|
||||
static struct {
|
||||
pthread_mutex_t mu;
|
||||
bool ready; /* a result is waiting for the main loop */
|
||||
int n;
|
||||
dns_addr_t addrs[MAX_HOSTS];
|
||||
} g_dns = { PTHREAD_MUTEX_INITIALIZER, false, 0, {{0, ""}} };
|
||||
|
||||
/* Smallest A/AAAA TTL (CNAMEs included) of host, or -1 if it cannot be determined. */
|
||||
static long dns_host_ttl(const char *host, int af_filter) {
|
||||
unsigned char probe[sizeof(struct in6_addr)];
|
||||
if (inet_pton(AF_INET, host, probe) == 1 || inet_pton(AF_INET6, host, probe) == 1) return -1;
|
||||
|
||||
long best = -1;
|
||||
int types[2] = { ns_t_a, ns_t_aaaa };
|
||||
for (int t = 0; t < 2; t++) {
|
||||
if (af_filter == AF_INET && t == 1) continue;
|
||||
if (af_filter == AF_INET6 && t == 0) continue;
|
||||
unsigned char ans[NS_PACKETSZ * 2];
|
||||
int len = res_search(host, ns_c_in, types[t], ans, sizeof(ans));
|
||||
ns_msg msg;
|
||||
if (len < 0 || ns_initparse(ans, len, &msg) < 0) continue;
|
||||
long min = -1; bool found = false;
|
||||
for (int i = 0; i < ns_msg_count(msg, ns_s_an); i++) {
|
||||
ns_rr rr;
|
||||
if (ns_parserr(&msg, ns_s_an, i, &rr) < 0) continue;
|
||||
int ty = ns_rr_type(rr);
|
||||
if (ty != types[t] && ty != ns_t_cname) continue;
|
||||
if (ty == types[t]) found = true;
|
||||
if (min < 0 || (long)ns_rr_ttl(rr) < min) min = (long)ns_rr_ttl(rr);
|
||||
}
|
||||
if (found && (best < 0 || min < best)) best = min;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* Seconds until the next resolution: the smallest known TTL, floored at DNS_MIN_TTL. */
|
||||
static int dns_refresh_delay(const dns_ctx_t *x) {
|
||||
long best = -1;
|
||||
for (int i = 0; i < x->nhost; i++) {
|
||||
long t = dns_host_ttl(x->hosts[i], x->af_filter);
|
||||
if (t >= 0 && (best < 0 || t < best)) best = t;
|
||||
}
|
||||
if (best < 0) return DNS_DEFAULT_TTL;
|
||||
return best < DNS_MIN_TTL ? DNS_MIN_TTL : (best > 86400 ? 86400 : (int)best);
|
||||
}
|
||||
|
||||
/* Every (family, address) the hosts resolve to, deduplicated; -1 if any lookup fails. */
|
||||
static int dns_resolve_all(const dns_ctx_t *x, dns_addr_t *out, int max) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < x->nhost; i++) {
|
||||
struct addrinfo hints = {0}, *res = NULL;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
hints.ai_protocol = IPPROTO_UDP;
|
||||
hints.ai_family = x->af_filter;
|
||||
char ps[16]; snprintf(ps, sizeof(ps), "%d", x->port);
|
||||
if (getaddrinfo(x->hosts[i], ps, &hints, &res) != 0) {
|
||||
LOGW("cannot resolve %s", x->hosts[i]);
|
||||
return -1;
|
||||
}
|
||||
for (struct addrinfo *ai = res; ai && n < max; ai = ai->ai_next) {
|
||||
dns_addr_t a; a.af = ai->ai_family;
|
||||
void *ip = (ai->ai_family == AF_INET)
|
||||
? (void *)&((struct sockaddr_in *)ai->ai_addr)->sin_addr
|
||||
: (void *)&((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr;
|
||||
inet_ntop(ai->ai_family, ip, a.addr, sizeof(a.addr));
|
||||
bool dup = false;
|
||||
for (int k = 0; k < n; k++) if (strcmp(out[k].addr, a.addr) == 0) dup = true;
|
||||
if (!dup) out[n++] = a;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void *dns_thread(void *arg) {
|
||||
const dns_ctx_t *x = arg;
|
||||
int delay = dns_refresh_delay(x);
|
||||
while (g_running) {
|
||||
for (int s = 0; s < delay && g_running; s++) sleep(1);
|
||||
if (!g_running) break;
|
||||
dns_addr_t tmp[MAX_HOSTS];
|
||||
int n = dns_resolve_all(x, tmp, MAX_HOSTS);
|
||||
if (n <= 0) { delay = DNS_FAILED_RETRY; continue; }
|
||||
pthread_mutex_lock(&g_dns.mu);
|
||||
memcpy(g_dns.addrs, tmp, sizeof(tmp));
|
||||
g_dns.n = n; g_dns.ready = true;
|
||||
pthread_mutex_unlock(&g_dns.mu);
|
||||
delay = dns_refresh_delay(x);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Point c at a new server address; the socket is replaced if the family changed. */
|
||||
static void conn_retarget(conn_t *c, int af, const char *addr) {
|
||||
LOGI("server address %s -> %s", c->addr, addr);
|
||||
pthread_mutex_lock(&c->mu);
|
||||
if (af != c->af) {
|
||||
int fd = socket(af, SOCK_DGRAM, 0);
|
||||
if (fd < 0) { LOGE("socket: %s", strerror(errno)); pthread_mutex_unlock(&c->mu); return; }
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
|
||||
if (c->sockfd >= 0) close(c->sockfd);
|
||||
c->sockfd = fd; c->af = af;
|
||||
}
|
||||
snprintf(c->addr, sizeof(c->addr), "%s", addr);
|
||||
pthread_mutex_unlock(&c->mu);
|
||||
}
|
||||
|
||||
/* Main-loop side: follow the latest resolution. Mirrors hbd.client.dnsttl.retarget(). Gone
|
||||
* addresses are re-pointed at new ones in list order, so g_conns[0] (the one plugin data
|
||||
* uses) survives as long as anything resolves; the rest are retired, surplus new ones added. */
|
||||
static void dns_apply(int *conn_id, const char *iam, int port) {
|
||||
dns_addr_t r[MAX_HOSTS]; int nr;
|
||||
pthread_mutex_lock(&g_dns.mu);
|
||||
if (!g_dns.ready) { pthread_mutex_unlock(&g_dns.mu); return; }
|
||||
memcpy(r, g_dns.addrs, sizeof(r)); nr = g_dns.n; g_dns.ready = false;
|
||||
pthread_mutex_unlock(&g_dns.mu);
|
||||
|
||||
int gone[MAX_HOSTS], ng = 0, fresh[MAX_HOSTS], nf = 0;
|
||||
for (int i = 0; i < g_nconns; i++) {
|
||||
bool wanted = false;
|
||||
for (int j = 0; j < nr; j++) if (strcmp(g_conns[i].addr, r[j].addr) == 0) wanted = true;
|
||||
if (!wanted) gone[ng++] = i;
|
||||
}
|
||||
for (int j = 0; j < nr; j++) {
|
||||
bool have = false;
|
||||
for (int i = 0; i < g_nconns; i++) if (strcmp(g_conns[i].addr, r[j].addr) == 0) have = true;
|
||||
if (!have) fresh[nf++] = j;
|
||||
}
|
||||
|
||||
int paired = ng < nf ? ng : nf;
|
||||
for (int k = 0; k < paired; k++) conn_retarget(&g_conns[gone[k]], r[fresh[k]].af, r[fresh[k]].addr);
|
||||
|
||||
/* drop[] holds connection indexes to retire, in ascending order */
|
||||
int drop[MAX_HOSTS], nd = 0;
|
||||
for (int k = paired; k < ng; k++) drop[nd++] = gone[k];
|
||||
if (nd > 0 && drop[0] == 0) {
|
||||
int keep = -1;
|
||||
for (int i = 1; i < g_nconns && keep < 0; i++) {
|
||||
bool isgone = false;
|
||||
for (int k = 0; k < ng; k++) if (gone[k] == i) isgone = true;
|
||||
if (!isgone) keep = i;
|
||||
}
|
||||
if (keep > 0) {
|
||||
conn_retarget(&g_conns[0], g_conns[keep].af, g_conns[keep].addr);
|
||||
drop[0] = keep;
|
||||
for (int a = 0; a < nd - 1 && drop[a] > drop[a + 1]; a++) {
|
||||
int tmp = drop[a]; drop[a] = drop[a + 1]; drop[a + 1] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int k = nd - 1; k >= 0; k--) {
|
||||
int i = drop[k];
|
||||
if (i == 0) continue;
|
||||
LOGI("dropping server address %s", g_conns[i].addr);
|
||||
conn_close(&g_conns[i]);
|
||||
pthread_mutex_destroy(&g_conns[i].mu);
|
||||
int last = --g_nconns;
|
||||
if (i != last) {
|
||||
memcpy(&g_conns[i], &g_conns[last], sizeof(conn_t));
|
||||
pthread_mutex_init(&g_conns[i].mu, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
for (int k = paired; k < nf && g_nconns < MAX_HOSTS; k++) {
|
||||
conn_t *c = &g_conns[g_nconns];
|
||||
memset(c, 0, sizeof(*c));
|
||||
c->conn_id = (*conn_id)++; c->port = port;
|
||||
c->af = r[fresh[k]].af; c->sockfd = -1;
|
||||
snprintf(c->name, sizeof(c->name), "%s", iam);
|
||||
snprintf(c->addr, sizeof(c->addr), "%s", r[fresh[k]].addr);
|
||||
if (conn_open(c)) { g_nconns++; LOGI("adding server address %s", c->addr); }
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
bool do_boot = false;
|
||||
bool do_daemon = false;
|
||||
@@ -1776,9 +1971,17 @@ int main(int argc, char **argv) {
|
||||
else LOGW("pthread_create(probe): %s", strerror(prc));
|
||||
}
|
||||
|
||||
/* Re-resolution at the DNS TTL; detached, the process exit reaps it */
|
||||
dns_ctx_t dns_ctx = { hosts, nhost, cfg.hb_port, af_filter };
|
||||
pthread_t dtid;
|
||||
int drc = pthread_create(&dtid, NULL, dns_thread, &dns_ctx);
|
||||
if (drc == 0) pthread_detach(dtid);
|
||||
else LOGW("pthread_create(dns): %s", strerror(drc));
|
||||
|
||||
/* Main heartbeat + receive loop */
|
||||
time_t last_hb = 0;
|
||||
while (g_running) {
|
||||
dns_apply(&conn_id, iam, cfg.hb_port);
|
||||
time_t now = time(NULL);
|
||||
if (now - last_hb >= cfg.interval) {
|
||||
for (int i = 0; i < g_nconns; i++) {
|
||||
|
||||
@@ -226,6 +226,55 @@ static int live_mode(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- server address re-resolution (dns_apply) ---- */
|
||||
|
||||
static void dns_conn(int i, int af, const char *addr) {
|
||||
memset(&g_conns[i], 0, sizeof(conn_t));
|
||||
g_conns[i].conn_id = i + 1; g_conns[i].af = af; g_conns[i].port = 5;
|
||||
snprintf(g_conns[i].addr, sizeof(g_conns[i].addr), "%s", addr);
|
||||
conn_open(&g_conns[i]);
|
||||
}
|
||||
|
||||
static void dns_push(int n, const dns_addr_t *a) {
|
||||
memcpy(g_dns.addrs, a, n * sizeof(*a)); g_dns.n = n; g_dns.ready = true;
|
||||
}
|
||||
|
||||
static void test_dns_apply_follows_changes(void) {
|
||||
int id = 3;
|
||||
dns_conn(0, AF_INET, "1.1.1.1"); dns_conn(1, AF_INET6, "2001:db8::1"); g_nconns = 2;
|
||||
|
||||
dns_addr_t moved[2] = {{AF_INET, "9.9.9.9"}, {AF_INET6, "2001:db8::9"}};
|
||||
dns_push(2, moved); dns_apply(&id, "me", 5);
|
||||
CHECK(g_nconns == 2);
|
||||
CHECK_STR(g_conns[0].addr, "9.9.9.9");
|
||||
CHECK_STR(g_conns[1].addr, "2001:db8::9");
|
||||
|
||||
/* the first connection's address vanishes and nothing replaces it: it survives, the other goes */
|
||||
dns_addr_t v6only[1] = {{AF_INET6, "2001:db8::9"}};
|
||||
dns_push(1, v6only); dns_apply(&id, "me", 5);
|
||||
CHECK(g_nconns == 1);
|
||||
CHECK_STR(g_conns[0].addr, "2001:db8::9");
|
||||
CHECK(g_conns[0].af == AF_INET6);
|
||||
|
||||
dns_addr_t grown[2] = {{AF_INET6, "2001:db8::9"}, {AF_INET, "5.5.5.5"}};
|
||||
dns_push(2, grown); dns_apply(&id, "me", 5);
|
||||
CHECK(g_nconns == 2);
|
||||
CHECK_STR(g_conns[1].addr, "5.5.5.5");
|
||||
CHECK(g_conns[1].conn_id == 3);
|
||||
|
||||
dns_push(2, grown); dns_apply(&id, "me", 5); /* unchanged: nothing happens */
|
||||
CHECK(g_nconns == 2 && id == 4);
|
||||
for (int i = 0; i < g_nconns; i++) conn_close(&g_conns[i]);
|
||||
g_nconns = 0;
|
||||
}
|
||||
|
||||
static void test_dns_ttl_unknown_for_ip_literals(void) {
|
||||
CHECK(dns_host_ttl("192.0.2.1", 0) == -1);
|
||||
CHECK(dns_host_ttl("2001:db8::1", 0) == -1);
|
||||
const char *h[] = {"192.0.2.1"}; dns_ctx_t x = {h, 1, 5, 0};
|
||||
CHECK(dns_refresh_delay(&x) == DNS_DEFAULT_TTL);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1 && !strcmp(argv[1], "--live")) return live_mode();
|
||||
test_gateway_parsers();
|
||||
@@ -238,6 +287,8 @@ int main(int argc, char **argv) {
|
||||
test_route_loss_then_return_pings_immediately();
|
||||
test_missing_ping_stays_unknown_even_after_route_loss();
|
||||
test_unsupported_family_is_unknown();
|
||||
test_dns_apply_follows_changes();
|
||||
test_dns_ttl_unknown_for_ip_literals();
|
||||
printf("%d checks, %d failed\n", g_run, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user