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:
+167
-9
@@ -6,6 +6,12 @@
|
||||
*
|
||||
* Drop on any host and run: ./hbc_mini <hbd-host>
|
||||
* 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");
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user