From adbd2f9b466f478a7a14249ff5a071e760224c26 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Tue, 22 Sep 2026 04:50:17 -0400 Subject: [PATCH] feat(mini-c): send p4/p6 presence flags and connectivity metric from hbc_mini.c Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Uc9V1v7iXruubpoLiSE24M --- scripts/c/hbc_mini.c | 180 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 169 insertions(+), 11 deletions(-) diff --git a/scripts/c/hbc_mini.c b/scripts/c/hbc_mini.c index 1736c61..e1aa3d3 100644 --- a/scripts/c/hbc_mini.c +++ b/scripts/c/hbc_mini.c @@ -6,6 +6,12 @@ * * Drop on any host and run: ./hbc_mini * 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__ @@ -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->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; double rtt = ops->ping(af, p->target[f]); 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; } -/* --- replaced by the real implementations in the next task --- */ -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; } -static double sys_ping(int af, const char *t) { (void)af; (void)t; return -2.0; } -static void sys_gateway(int af, char *o, size_t n) { (void)af; (void)n; o[0] = '\0'; } -static double mono_ts(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); 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 @@ -1278,16 +1420,26 @@ static void plugin_network_monitor(conn_t *c, const config_t *cfg) { json[jlen++] = '}'; json[jlen] = '\0'; memcpy(g_net_prev, curr, cn * sizeof(net_stat_t)); g_net_prev_n = cn; g_net_prev_ts = now; - if (first) { free(json); return; } - kvdict_t d; kv_clear(&d); kv_set(&d, "plugin", "network_monitor"); - char *jval = malloc(MAX_VAL + 1); - if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "interfaces", jval); free(jval); } + if (!first) { + char *jval = malloc(MAX_VAL + 1); + 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_int(&d, "_interval", cfg->net_interval); conn_send(c, "PLG", &d); - free(json); LOGD("sent network_monitor"); } @@ -1524,6 +1676,7 @@ int main(int argc, char **argv) { config_t cfg; config_load(&cfg, cfgpath); + probe_init(&g_probe, &cfg); if (do_daemon) { daemonize(); @@ -1606,6 +1759,9 @@ int main(int argc, char **argv) { pthread_t tid; 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 */ time_t last_hb = 0; 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_dbl(&hb, "rtt", g_conns[i].rtt); kv_set_int(&hb, "interval", cfg.interval); + if (g_probe_enabled) probe_flags(&g_probe, &hb); conn_send(&g_conns[i], "HTB", &hb); } last_hb = now; @@ -1642,6 +1799,7 @@ int main(int argc, char **argv) { pthread_cancel(tid); pthread_join(tid, NULL); + if (probe_running) pthread_join(ptid, NULL); if (send_shutdown) { kvdict_t sh; kv_clear(&sh);