Files
heartbeat/scripts/c/test_probe.c
T
andreasandClaude Sonnet 5.5 885d5dc937 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>
2026-10-28 04:23:55 -04:00

295 lines
12 KiB
C

/* Unit tests for the connectivity-probe helpers in hbc_mini.c.
* Build and run: make test (live check of this host: ./test_probe --live)
* The source file is included directly so its static functions can be exercised; its main()
* is renamed out of the way.
*/
#define main hbc_mini_main
#include "hbc_mini.c"
#undef main
static int g_run = 0, g_fail = 0;
#define CHECK(cond) do { g_run++; if (!(cond)) { g_fail++; \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0)
#define CHECK_STR(a, b) do { g_run++; if (strcmp((a), (b)) != 0) { g_fail++; \
fprintf(stderr, "FAIL %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, (a), (b)); } } while (0)
static const char PROC_V4[] =
"Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n"
"wlan0\t00000000\t0100A8C0\t0003\t0\t0\t600\t00000000\t0\t0\t0\n"
"eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\n"
"wlan0\t0000A8C0\t00000000\t0001\t0\t0\t600\t00FFFFFF\t0\t0\t0\n";
static const char PROC_V6[] =
"00000000000000000000000000000000 00 00000000000000000000000000000000 00 "
"fe800000000000000000000000000001 00000400 00000001 00000000 00000003 wlan0\n"
"00000000000000000000000000000000 00 00000000000000000000000000000000 00 "
"00000000000000000000000000000000 ffffffff 00000001 00000000 00200200 lo\n"
"fe800000000000000000000000000000 40 00000000000000000000000000000000 00 "
"00000000000000000000000000000000 00000100 00000001 00000000 00000001 wlan0\n";
static const char ROUTE_GET[] =
" route to: 0.0.0.0\ndestination: 0.0.0.0\n mask: 0.0.0.0\n"
" gateway: 192.0.2.1\n fib: 0\n interface: vtnet0\n";
static const char LINUX_PING[] =
"PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.\n"
"64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=11.2 ms\n\n"
"--- 1.1.1.1 ping statistics ---\n"
"1 packets transmitted, 1 received, 0% packet loss, time 0ms\n"
"rtt min/avg/max/mdev = 11.234/11.234/11.234/0.000 ms\n";
static const char FREEBSD_PING[] =
"PING 1.1.1.1 (1.1.1.1): 56 data bytes\n"
"64 bytes from 1.1.1.1: icmp_seq=0 ttl=59 time=4.006 ms\n\n"
"--- 1.1.1.1 ping statistics ---\n"
"1 packets transmitted, 1 packets received, 0.0% packet loss, 1 packets out of wait time\n"
"round-trip min/avg/max/stddev = 4.006/4.006/4.006/0.000 ms\n";
static void test_gateway_parsers(void) {
char gw[64];
gw_parse_proc_v4(PROC_V4, gw, sizeof(gw));
CHECK_STR(gw, "192.168.1.1"); /* eth0, metric 100 beats wlan0, 600 */
gw_parse_proc_v4("Iface\tDestination\n", gw, sizeof(gw));
CHECK_STR(gw, "");
gw_parse_proc_v6(PROC_V6, gw, sizeof(gw));
CHECK_STR(gw, "fe80::1"); /* reject and non-default lines skipped */
gw_parse_route_get(ROUTE_GET, gw, sizeof(gw));
CHECK_STR(gw, "192.0.2.1");
gw_parse_route_get("route: writing to routing socket: not in table\n", gw, sizeof(gw));
CHECK_STR(gw, "");
}
static void test_ping_parser(void) {
CHECK(ping_parse_rtt(LINUX_PING) > 11.23 && ping_parse_rtt(LINUX_PING) < 11.24);
CHECK(ping_parse_rtt(FREEBSD_PING) > 4.0 && ping_parse_rtt(FREEBSD_PING) < 4.01);
CHECK(ping_parse_rtt("1 packets transmitted, 0 received, 100% packet loss\n") == -1.0);
CHECK(ping_parse_rtt("sh: 1: ping: not found\n") == -2.0);
CHECK(ping_parse_rtt("ping: command not found\n") == -2.0);
}
/* ---- scriptable network for probe_step ------------------------------------------------ */
static struct {
char addr[2][INET6_ADDRSTRLEN]; /* "" = no route */
char gw[2][INET6_ADDRSTRLEN];
double rtt[2]; /* -1 no reply, -2 ping unavailable */
int unsupported[2];
int npings[2];
} net;
static int fake_local(int af, const char *t, char *out, size_t n) {
int f = af == AF_INET6; (void)t;
if (net.unsupported[f]) return -1;
if (!net.addr[f][0]) return 0;
snprintf(out, n, "%s", net.addr[f]);
return 1;
}
static double fake_ping(int af, const char *t) { int f = af == AF_INET6; (void)t; net.npings[f]++; return net.rtt[f]; }
static void fake_gw(int af, char *out, size_t n) { snprintf(out, n, "%s", net.gw[af == AF_INET6]); }
static const probe_ops_t FAKE = { fake_local, fake_ping, fake_gw };
static void net_reset(void) {
memset(&net, 0, sizeof(net));
snprintf(net.addr[0], INET6_ADDRSTRLEN, "192.0.2.5");
snprintf(net.addr[1], INET6_ADDRSTRLEN, "2001:db8::5");
snprintf(net.gw[0], INET6_ADDRSTRLEN, "192.0.2.1");
snprintf(net.gw[1], INET6_ADDRSTRLEN, "fe80::1");
net.rtt[0] = 10.0; net.rtt[1] = 20.0;
}
static void probe_reset(probe_t *p) {
memset(p, 0, sizeof(*p));
snprintf(p->target[0], sizeof(p->target[0]), "1.1.1.1");
snprintf(p->target[1], sizeof(p->target[1]), "2606:4700:4700::1111");
p->probe_interval = 60; p->retry_interval = 10; p->cheap_gateway = true;
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);
}
static void step_both(probe_t *p, double now) {
probe_step(p, &p->fam[0], 0, &FAKE, now);
probe_step(p, &p->fam[1], 1, &FAKE, now);
}
static void test_present_with_details(void) {
net_reset(); probe_t p; probe_reset(&p);
step_both(&p, 1000);
CHECK(p.fam[0].present == 1 && p.fam[1].present == 1);
CHECK_STR(p.fam[0].addr, "192.0.2.5");
CHECK_STR(p.fam[0].gateway, "192.0.2.1");
CHECK(p.fam[0].rtt == 10.0);
kvdict_t d; kv_clear(&d);
probe_flags(&p, &d);
CHECK_STR(kv_get(&d, "p4") ? kv_get(&d, "p4") : "(null)", "1");
CHECK_STR(kv_get(&d, "p6") ? kv_get(&d, "p6") : "(null)", "1");
char js[512];
CHECK(probe_metric_json(&p, js, sizeof(js)) > 0);
CHECK(strstr(js, "\"ipv6\":{\"present\":true,\"addr\":\"2001:db8::5\",\"gateway\":\"fe80::1\"") != NULL);
}
static void test_no_route_is_absent_and_wastes_no_ping(void) {
net_reset(); net.addr[1][0] = '\0'; probe_t p; probe_reset(&p);
step_both(&p, 1000);
CHECK(p.fam[1].present == 0 && net.npings[1] == 0 && net.npings[0] == 1);
kvdict_t d; kv_clear(&d);
probe_flags(&p, &d);
CHECK_STR(kv_get(&d, "p6") ? kv_get(&d, "p6") : "(null)", "0");
char js[512];
probe_metric_json(&p, js, sizeof(js));
CHECK(strstr(js, "\"ipv6\":{\"present\":false,\"addr\":null,\"gateway\":null,\"rtt\":null}") != NULL);
}
static void test_debounce_and_rate_limit(void) {
net_reset(); probe_t p; probe_reset(&p);
step_both(&p, 1000);
step_both(&p, 1000); /* cached: no second ping */
CHECK(net.npings[0] == 1 && net.npings[1] == 1);
net.rtt[1] = -1;
step_both(&p, 1060); /* first failure: still present */
CHECK(p.fam[1].present == 1);
step_both(&p, 1070); /* retry interval elapsed: flips */
CHECK(p.fam[1].present == 0);
net.rtt[1] = 25;
step_both(&p, 1080); /* first success recovers */
CHECK(p.fam[1].present == 1);
}
static void test_first_failure_is_unknown_and_omitted(void) {
net_reset(); net.rtt[0] = -1; probe_t p; probe_reset(&p);
step_both(&p, 1000);
CHECK(p.fam[0].present == PROBE_UNKNOWN);
kvdict_t d; kv_clear(&d);
probe_flags(&p, &d);
CHECK(kv_get(&d, "p4") == NULL && kv_get(&d, "p6") != NULL);
char js[512];
probe_metric_json(&p, js, sizeof(js));
CHECK(strstr(js, "\"ipv4\":{\"present\":null") != NULL);
}
static void test_address_or_gateway_change_repings(void) {
net_reset(); probe_t p; probe_reset(&p);
step_both(&p, 1000);
snprintf(net.addr[0], INET6_ADDRSTRLEN, "198.51.100.7");
step_both(&p, 1000);
CHECK(net.npings[0] == 2);
snprintf(net.gw[0], INET6_ADDRSTRLEN, "192.0.2.254");
step_both(&p, 1000);
CHECK(net.npings[0] == 3);
}
static void test_route_loss_then_return_pings_immediately(void) {
net_reset(); probe_t p; probe_reset(&p);
step_both(&p, 1000);
net.addr[1][0] = '\0';
step_both(&p, 1000);
CHECK(p.fam[1].present == 0);
snprintf(net.addr[1], INET6_ADDRSTRLEN, "2001:db8::5");
step_both(&p, 1000);
CHECK(net.npings[1] == 2 && p.fam[1].present == 1);
}
static void test_missing_ping_stays_unknown_even_after_route_loss(void) {
net_reset(); net.rtt[0] = -2; probe_t p; probe_reset(&p);
step_both(&p, 1000);
step_both(&p, 1600);
CHECK(p.fam[0].present == PROBE_UNKNOWN && net.npings[0] == 1);
net.addr[0][0] = '\0';
step_both(&p, 1700);
CHECK(p.fam[0].present == 0); /* no route is definitive */
snprintf(net.addr[0], INET6_ADDRSTRLEN, "192.0.2.5");
step_both(&p, 1800);
CHECK(p.fam[0].present == PROBE_UNKNOWN && net.npings[0] == 1);
}
static void test_unsupported_family_is_unknown(void) {
net_reset(); net.unsupported[1] = 1; probe_t p; probe_reset(&p);
step_both(&p, 1000);
CHECK(p.fam[1].present == PROBE_UNKNOWN && p.fam[0].present == 1);
}
static int live_mode(void) {
static const probe_ops_t sys_ops = { sys_local_address, sys_ping, sys_gateway };
probe_t p; probe_reset(&p);
p.cheap_gateway = false;
snprintf(p.target[0], sizeof(p.target[0]), "1.1.1.1");
for (int round = 0; round < 3; round++) {
double now = mono_ts();
for (int f = 0; f < 2; f++) probe_step(&p, &p.fam[f], f, &sys_ops, now);
sleep(round < 2 ? 11 : 0);
}
char js[512];
probe_metric_json(&p, js, sizeof(js));
kvdict_t d; kv_clear(&d);
probe_flags(&p, &d);
printf("%s\np4=%s p6=%s\n", js, kv_get(&d, "p4") ? kv_get(&d, "p4") : "-",
kv_get(&d, "p6") ? kv_get(&d, "p6") : "-");
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();
test_ping_parser();
test_present_with_details();
test_no_route_is_absent_and_wastes_no_ping();
test_debounce_and_rate_limit();
test_first_failure_is_unknown_and_omitted();
test_address_or_gateway_change_repings();
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;
}