feat(mini-c): send p4/p6 presence flags and connectivity metric from hbc_mini.c

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc9V1v7iXruubpoLiSE24M
This commit is contained in:
2026-09-22 04:50:17 -04:00
co-authored by Claude Sonnet 5
parent 248b17edca
commit adbd2f9b46
+167 -9
View File
@@ -6,6 +6,12 @@
* *
* Drop on any host and run: ./hbc_mini <hbd-host> * Drop on any host and run: ./hbc_mini <hbd-host>
* Config: ~/.hbc.json (same keys as Python version) * Config: ~/.hbc.json (same keys as Python version)
*
* Presence probe (built in): reports per protocol (IPv4/IPv6) whether the host has internet
* connectivity, as p4/p6 flags on every heartbeat, so hbd stops treating a missing IPv6 (or
* IPv4) path as an outage. Config, in the network_monitor section of ~/.hbc.json:
* probe_enabled (default true), probe_v4 (default 1.1.1.1), probe_v6 (default
* 2606:4700:4700::1111; IP literals only), probe_interval (default 60 s)
*/ */
#ifdef __linux__ #ifdef __linux__
@@ -1133,7 +1139,7 @@ static void probe_step(const probe_t *p, probe_fam_t *s, int f, const probe_ops_
snprintf(s->gateway, sizeof(s->gateway), "%s", gw); snprintf(s->gateway, sizeof(s->gateway), "%s", gw);
snprintf(s->last_key, sizeof(s->last_key), "%s", key); snprintf(s->last_key, sizeof(s->last_key), "%s", key);
if (s->ping_disabled) { s->present = PROBE_UNKNOWN; return; } /* cannot probe: unknown, never absent */ if (s->ping_disabled) { s->present = PROBE_UNKNOWN; return; } /* cannot probe: unknown, never absent, while a route exists */
if (!due) return; if (!due) return;
double rtt = ops->ping(af, p->target[f]); double rtt = ops->ping(af, p->target[f]);
if (rtt < -1.5) { s->ping_disabled = true; s->present = PROBE_UNKNOWN; return; } if (rtt < -1.5) { s->ping_disabled = true; s->present = PROBE_UNKNOWN; return; }
@@ -1179,11 +1185,147 @@ static int probe_metric_json(probe_t *p, char *out, size_t n) {
return (int)len; return (int)len;
} }
/* --- replaced by the real implementations in the next task --- */ static double mono_ts(void) {
static int sys_local_address(int af, const char *t, char *o, size_t n) { (void)af; (void)t; (void)o; (void)n; return -1; } struct timespec ts;
static double sys_ping(int af, const char *t) { (void)af; (void)t; return -2.0; } clock_gettime(CLOCK_MONOTONIC, &ts);
static void sys_gateway(int af, char *o, size_t n) { (void)af; (void)n; o[0] = '\0'; } return ts.tv_sec + ts.tv_nsec * 1e-9;
static double mono_ts(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec * 1e-9; } }
/* Source address the kernel would use to reach target (an IP literal). A UDP connect() sends
* nothing. Returns 1 = route (address in out), 0 = no route, -1 = family unsupported or the
* target is not an IP literal (unknown, never absent). */
static int sys_local_address(int af, const char *target, char *out, size_t n) {
int fd = socket(af, SOCK_DGRAM, 0);
if (fd < 0) return -1;
struct sockaddr_storage sa; socklen_t sl;
memset(&sa, 0, sizeof(sa));
if (af == AF_INET) {
struct sockaddr_in *s4 = (struct sockaddr_in *)&sa;
s4->sin_family = AF_INET; s4->sin_port = htons(9);
if (inet_pton(AF_INET, target, &s4->sin_addr) != 1) { close(fd); return -1; }
sl = sizeof(*s4);
} else {
struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&sa;
s6->sin6_family = AF_INET6; s6->sin6_port = htons(9);
if (inet_pton(AF_INET6, target, &s6->sin6_addr) != 1) { close(fd); return -1; }
sl = sizeof(*s6);
}
int rc = 0;
if (connect(fd, (struct sockaddr *)&sa, sl) == 0) {
struct sockaddr_storage me; socklen_t ml = sizeof(me);
if (getsockname(fd, (struct sockaddr *)&me, &ml) == 0) {
const void *src = (af == AF_INET)
? (const void *)&((struct sockaddr_in *)&me)->sin_addr
: (const void *)&((struct sockaddr_in6 *)&me)->sin6_addr;
if (inet_ntop(af, src, out, (socklen_t)n)) rc = 1;
}
}
close(fd);
return rc;
}
static char *slurp(FILE *f) {
size_t cap = 4096, len = 0;
char *buf = malloc(cap);
if (!buf) return NULL;
size_t r;
while ((r = fread(buf + len, 1, cap - len - 1, f)) > 0) {
len += r;
if (len + 1 >= cap) {
if (cap >= 65536) break;
cap *= 2;
char *nb = realloc(buf, cap);
if (!nb) { free(buf); return NULL; }
buf = nb;
}
}
buf[len] = '\0';
return buf;
}
static bool g_have_timeout = false; /* timeout(1) exists: bounds ping6 on the BSDs */
/* One echo request: RTT in ms, -1.0 no reply, -2.0 ping could not be run. The target is an IP
* literal from the config; anything else is refused before it can reach the shell. */
static double sys_ping(int af, const char *target) {
for (const char *c = target; *c; c++)
if (!(isxdigit((unsigned char)*c) || *c == '.' || *c == ':')) return -2.0;
char cmd[256];
#ifdef __linux__
snprintf(cmd, sizeof(cmd), "ping -c 1 -W 2 -w 3 %s %s 2>&1", af == AF_INET ? "-4" : "-6", target);
#else
/* BSD ping: -W is in milliseconds, and IPv6 is reached through ping6 */
if (af == AF_INET)
snprintf(cmd, sizeof(cmd), "%sping -c 1 -W 2000 %s 2>&1", g_have_timeout ? "timeout 4 " : "", target);
else
snprintf(cmd, sizeof(cmd), "%sping6 -c 1 %s 2>&1", g_have_timeout ? "timeout 4 " : "", target);
#endif
FILE *f = popen(cmd, "r");
if (!f) return -2.0;
char *out = slurp(f);
pclose(f);
if (!out) return -1.0;
double rtt = ping_parse_rtt(out);
free(out);
return rtt;
}
/* Default gateway for the family, "" when unknown. Best effort; never affects presence. */
static void sys_gateway(int af, char *out, size_t n) {
out[0] = '\0';
#ifdef __linux__
FILE *f = fopen(af == AF_INET ? "/proc/net/route" : "/proc/net/ipv6_route", "r");
if (!f) return;
char *text = slurp(f);
fclose(f);
if (!text) return;
if (af == AF_INET) gw_parse_proc_v4(text, out, n); else gw_parse_proc_v6(text, out, n);
free(text);
#else
FILE *f = popen(af == AF_INET ? "route -n get default 2>/dev/null"
: "route -n get -inet6 default 2>/dev/null", "r");
if (!f) return;
char *text = slurp(f);
pclose(f);
if (!text) return;
gw_parse_route_get(text, out, n);
free(text);
#endif
}
static probe_t g_probe;
static bool g_probe_enabled = false;
static void probe_init(probe_t *p, const config_t *cfg) {
memset(p, 0, sizeof(*p));
snprintf(p->target[0], sizeof(p->target[0]), "%s", cfg->probe_v4);
snprintf(p->target[1], sizeof(p->target[1]), "%s", cfg->probe_v6);
p->probe_interval = cfg->probe_interval > 0 ? cfg->probe_interval : 60;
p->retry_interval = 10;
#ifdef __linux__
p->cheap_gateway = true; /* /proc read; elsewhere a subprocess only when due */
#endif
p->fam[0].present = p->fam[1].present = PROBE_UNKNOWN;
p->fam[0].rtt = p->fam[1].rtt = -1;
pthread_mutex_init(&p->mu, NULL);
g_have_timeout = system("command -v timeout >/dev/null 2>&1") == 0;
g_probe_enabled = cfg->probe_enabled;
}
static void *probe_thread(void *arg) {
probe_t *p = arg;
static const probe_ops_t ops = { sys_local_address, sys_ping, sys_gateway };
while (g_running) {
double now = mono_ts();
for (int f = 0; f < 2 && g_running; f++) {
pthread_mutex_lock(&p->mu); probe_fam_t tmp = p->fam[f]; pthread_mutex_unlock(&p->mu);
probe_step(p, &tmp, f, &ops, now); /* may block in ping: no lock held */
pthread_mutex_lock(&p->mu); p->fam[f] = tmp; pthread_mutex_unlock(&p->mu);
}
for (int i = 0; i < PROBE_TIER1 && g_running; i++) sleep(1);
}
return NULL;
}
/* ============================================================ /* ============================================================
* Plugin: network_monitor * Plugin: network_monitor
@@ -1278,16 +1420,26 @@ static void plugin_network_monitor(conn_t *c, const config_t *cfg) {
json[jlen++] = '}'; json[jlen] = '\0'; json[jlen++] = '}'; json[jlen] = '\0';
memcpy(g_net_prev, curr, cn * sizeof(net_stat_t)); memcpy(g_net_prev, curr, cn * sizeof(net_stat_t));
g_net_prev_n = cn; g_net_prev_ts = now; g_net_prev_n = cn; g_net_prev_ts = now;
if (first) { free(json); return; }
kvdict_t d; kv_clear(&d); kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "network_monitor"); kv_set(&d, "plugin", "network_monitor");
if (!first) {
char *jval = malloc(MAX_VAL + 1); char *jval = malloc(MAX_VAL + 1);
if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "interfaces", jval); free(jval); } if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "interfaces", jval); free(jval); }
}
bool have_conn = false;
if (g_probe_enabled) {
char cj[512];
cj[0] = '@';
if (probe_metric_json(&g_probe, cj + 1, sizeof(cj) - 1) > 0) {
kv_set(&d, "connectivity", cj);
have_conn = true;
}
}
free(json);
if (first && !have_conn) return;
kv_set_dbl(&d, "_timestamp", now_ts()); kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", cfg->net_interval); kv_set_int(&d, "_interval", cfg->net_interval);
conn_send(c, "PLG", &d); conn_send(c, "PLG", &d);
free(json);
LOGD("sent network_monitor"); LOGD("sent network_monitor");
} }
@@ -1524,6 +1676,7 @@ int main(int argc, char **argv) {
config_t cfg; config_t cfg;
config_load(&cfg, cfgpath); config_load(&cfg, cfgpath);
probe_init(&g_probe, &cfg);
if (do_daemon) { if (do_daemon) {
daemonize(); daemonize();
@@ -1606,6 +1759,9 @@ int main(int argc, char **argv) {
pthread_t tid; pthread_t tid;
pthread_create(&tid, NULL, monitor_thread, &tctx); pthread_create(&tid, NULL, monitor_thread, &tctx);
pthread_t ptid; bool probe_running = false;
if (g_probe_enabled && pthread_create(&ptid, NULL, probe_thread, &g_probe) == 0) probe_running = true;
/* Main heartbeat + receive loop */ /* Main heartbeat + receive loop */
time_t last_hb = 0; time_t last_hb = 0;
while (g_running) { while (g_running) {
@@ -1616,6 +1772,7 @@ int main(int argc, char **argv) {
kv_set_int(&hb, "acks", g_conns[i].ack_count); kv_set_int(&hb, "acks", g_conns[i].ack_count);
kv_set_dbl(&hb, "rtt", g_conns[i].rtt); kv_set_dbl(&hb, "rtt", g_conns[i].rtt);
kv_set_int(&hb, "interval", cfg.interval); kv_set_int(&hb, "interval", cfg.interval);
if (g_probe_enabled) probe_flags(&g_probe, &hb);
conn_send(&g_conns[i], "HTB", &hb); conn_send(&g_conns[i], "HTB", &hb);
} }
last_hb = now; last_hb = now;
@@ -1642,6 +1799,7 @@ int main(int argc, char **argv) {
pthread_cancel(tid); pthread_cancel(tid);
pthread_join(tid, NULL); pthread_join(tid, NULL);
if (probe_running) pthread_join(ptid, NULL);
if (send_shutdown) { if (send_shutdown) {
kvdict_t sh; kv_clear(&sh); kvdict_t sh; kv_clear(&sh);