diff --git a/scripts/c/.gitignore b/scripts/c/.gitignore index d54821c..597de2b 100644 --- a/scripts/c/.gitignore +++ b/scripts/c/.gitignore @@ -1,2 +1,3 @@ hbc_mini hbc_mini_dbg +test_probe diff --git a/scripts/c/Makefile b/scripts/c/Makefile index 8b4c81b..2831662 100644 --- a/scripts/c/Makefile +++ b/scripts/c/Makefile @@ -7,7 +7,7 @@ SRC = hbc_mini.c # FreeBSD/NetBSD keep zlib in base; no extra flags needed. # On some NetBSD installs pthreads may need -lpthread from pkgsrc. -.PHONY: all clean debug +.PHONY: all clean debug test all: $(TARGET) @@ -17,5 +17,11 @@ $(TARGET): $(SRC) debug: $(SRC) $(CC) -g -fsanitize=address,undefined -o $(TARGET)_dbg $< $(LDFLAGS) +test: test_probe + ./test_probe + +test_probe: test_probe.c $(SRC) + $(CC) $(CFLAGS) -o $@ test_probe.c $(LDFLAGS) + clean: - rm -f $(TARGET) $(TARGET)_dbg + rm -f $(TARGET) $(TARGET)_dbg test_probe diff --git a/scripts/c/hbc_mini.c b/scripts/c/hbc_mini.c index 4ca7c6a..1736c61 100644 --- a/scripts/c/hbc_mini.c +++ b/scripts/c/hbc_mini.c @@ -396,6 +396,10 @@ typedef struct { char net_skip[MAX_NET_SKIP][64]; int net_skip_count; + + bool probe_enabled; + int probe_interval; + char probe_v4[64], probe_v6[64]; } config_t; static void config_defaults(config_t *c) { @@ -410,6 +414,10 @@ static void config_defaults(config_t *c) { c->net_interval = 300; snprintf(c->net_skip[0], 64, "lo"); c->net_skip_count = 1; + c->probe_enabled = true; + c->probe_interval = 60; + snprintf(c->probe_v4, sizeof(c->probe_v4), "1.1.1.1"); + snprintf(c->probe_v6, sizeof(c->probe_v6), "2606:4700:4700::1111"); } static void config_load(config_t *cfg, const char *path) { @@ -498,6 +506,10 @@ static void config_load(config_t *cfg, const char *path) { jval_t *net = plugins ? jget(plugins,"network_monitor") : jget(root,"network_monitor"); if (net && net->type == JT_OBJECT) { if ((v=jget(net,"interval"))) cfg->net_interval = jint(v, cfg->net_interval); + if ((v=jget(net,"probe_enabled"))) cfg->probe_enabled = jint(v, 1) != 0; + if ((v=jget(net,"probe_interval"))) cfg->probe_interval = jint(v, cfg->probe_interval); + if ((v=jget(net,"probe_v4"))) snprintf(cfg->probe_v4, sizeof(cfg->probe_v4), "%s", jstr(v, cfg->probe_v4)); + if ((v=jget(net,"probe_v6"))) snprintf(cfg->probe_v6, sizeof(cfg->probe_v6), "%s", jstr(v, cfg->probe_v6)); jval_t *sk = jget(net,"skip_interfaces"); if (sk && sk->type == JT_ARRAY) { cfg->net_skip_count = 0; @@ -984,6 +996,195 @@ static void plugin_disk_monitor(conn_t *c, const config_t *cfg) { LOGD("sent disk_monitor"); } +/* ============================================================ + * Connectivity probe: per-protocol internet presence (IPv4 / IPv6) + * + * Tier 1 (every PROBE_TIER1 s, sends no packets): a UDP connect() to the probe address shows + * whether a route exists and which source address would be used. Tier 2 (every probe_interval + * s, sooner after a failure or a change of address or gateway): one ICMP echo, only when tier 1 + * found a route. Absent = no route, or PROBE_FAIL_LIMIT consecutive failed echoes; present = + * one success; unknown (flag omitted) when the family cannot be probed. The result rides on + * every HTB as p4/p6 and inside the network_monitor metric. Mirrors hbd/client/connectivity.py. + * ============================================================ */ + +#define PROBE_UNKNOWN (-1) +#define PROBE_FAIL_LIMIT 2 +#define PROBE_TIER1 5 /* seconds between route checks */ + +typedef struct { + int present; /* PROBE_UNKNOWN, 0 = absent, 1 = present */ + char addr[INET6_ADDRSTRLEN]; + char gateway[INET6_ADDRSTRLEN]; + double rtt; /* ms, < 0 when unknown */ + int fails; /* consecutive failed echoes */ + double next_ping; /* monotonic seconds */ + char last_key[2 * INET6_ADDRSTRLEN + 2]; + bool ping_disabled; /* no usable ping binary */ +} probe_fam_t; + +typedef struct { + /* 1 = route exists (source address in out), 0 = no route, -1 = family unsupported/unusable */ + int (*local_address)(int af, const char *target, char *out, size_t n); + /* RTT in ms; -1 = no reply; -2 = ping unavailable */ + double (*ping)(int af, const char *target); + /* fills out with the default gateway, "" when unknown */ + void (*gateway)(int af, char *out, size_t n); +} probe_ops_t; + +typedef struct { + probe_fam_t fam[2]; /* [0] = IPv4, [1] = IPv6 */ + char target[2][64]; + double probe_interval, retry_interval; + bool cheap_gateway; /* gateway read is cheap (/proc), so read every tier 1 */ + pthread_mutex_t mu; /* guards fam[] between the probe thread and readers */ +} probe_t; + +/* Lowest-metric default gateway from /proc/net/route. The kernel prints the raw network-order + * u32 in host order, so it can be stored straight into s_addr on any endianness. */ +static void gw_parse_proc_v4(const char *text, char *out, size_t n) { + long best = -1; out[0] = '\0'; + const char *p = strchr(text, '\n'); /* skip the header line */ + while (p && *++p) { + char iface[64], dest[16], gw[16]; unsigned flags; int refcnt, use, metric; + if (sscanf(p, "%63s %15s %15s %x %d %d %d", iface, dest, gw, &flags, &refcnt, &use, &metric) == 7 + && !strcmp(dest, "00000000") && (flags & 0x2)) { + if (best < 0 || metric < best) { + struct in_addr a; a.s_addr = (uint32_t)strtoul(gw, NULL, 16); + if (inet_ntop(AF_INET, &a, out, n)) best = metric; + } + } + p = strchr(p, '\n'); + } +} + +/* Lowest-metric default gateway from /proc/net/ipv6_route (skips RTF_REJECT and "lo"). */ +static void gw_parse_proc_v6(const char *text, char *out, size_t n) { + long best = -1; out[0] = '\0'; + const char *zero = "00000000000000000000000000000000"; + for (const char *p = text; p && *p; ) { + char dest[40], src[40], nh[40], iface[64]; + unsigned plen, splen, metric, refcnt, use, flags; + if (sscanf(p, "%32s %x %32s %x %32s %x %x %x %x %63s", + dest, &plen, src, &splen, nh, &metric, &refcnt, &use, &flags, iface) == 10 + && !strcmp(dest, zero) && plen == 0 && strcmp(nh, zero) && strcmp(iface, "lo") + && (flags & 0x2) && !(flags & 0x200)) { + if (best < 0 || (long)metric < best) { + struct in6_addr a; memset(&a, 0, sizeof(a)); + bool ok = strlen(nh) == 32; + for (int i = 0; ok && i < 16; i++) { + unsigned b; + if (sscanf(nh + 2 * i, "%2x", &b) != 1) ok = false; else a.s6_addr[i] = (uint8_t)b; + } + if (ok && inet_ntop(AF_INET6, &a, out, n)) best = (long)metric; + } + } + p = strchr(p, '\n'); if (p) p++; + } +} + +/* Gateway from `route -n get [-inet6] default` (the BSDs). */ +static void gw_parse_route_get(const char *text, char *out, size_t n) { + out[0] = '\0'; + const char *p = strstr(text, "gateway:"); + if (!p) return; + p += 8; + while (*p == ' ' || *p == '\t') p++; + size_t i = 0; + while (*p && !isspace((unsigned char)*p) && i + 1 < n) out[i++] = *p++; + out[i] = '\0'; +} + +/* Average RTT from ping output: -1.0 no reply, -2.0 ping could not be run. Works for Linux + * ("rtt min/avg/max/mdev") and BSD ("round-trip min/avg/max/stddev"). */ +static double ping_parse_rtt(const char *out) { + if (strstr(out, "not found") || strstr(out, "No such file")) return -2.0; + const char *p = strstr(out, "min/avg/max"); + if (!p) return -1.0; + p = strchr(p, '='); + double mn, avg, mx; + if (p && sscanf(p + 1, " %lf/%lf/%lf", &mn, &avg, &mx) == 3) return avg; + return -1.0; +} + +/* One probe step for one family (f: 0 = IPv4, 1 = IPv6). Works on *s only, so the probe thread + * can run it on a private copy without holding the mutex during the (blocking) echo. */ +static void probe_step(const probe_t *p, probe_fam_t *s, int f, const probe_ops_t *ops, double now) { + int af = f ? AF_INET6 : AF_INET; + char addr[INET6_ADDRSTRLEN] = "", gw[INET6_ADDRSTRLEN] = ""; + + int r = ops->local_address(af, p->target[f], addr, sizeof(addr)); + if (r < 0) { s->present = PROBE_UNKNOWN; return; } + if (r == 0) { /* tier 1: no route => absent immediately */ + s->fails = 0; s->next_ping = 0; s->last_key[0] = '\0'; + s->present = 0; s->addr[0] = '\0'; s->gateway[0] = '\0'; s->rtt = -1; + return; + } + + if (p->cheap_gateway) ops->gateway(af, gw, sizeof(gw)); + else snprintf(gw, sizeof(gw), "%s", s->gateway); + char key[sizeof(s->last_key)]; + snprintf(key, sizeof(key), "%s|%s", addr, gw); + bool due = strcmp(key, s->last_key) != 0 || now >= s->next_ping; + if (due && !p->cheap_gateway) { + ops->gateway(af, gw, sizeof(gw)); + snprintf(key, sizeof(key), "%s|%s", addr, gw); + } + snprintf(s->addr, sizeof(s->addr), "%s", addr); + 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 (!due) return; + double rtt = ops->ping(af, p->target[f]); + if (rtt < -1.5) { s->ping_disabled = true; s->present = PROBE_UNKNOWN; return; } + if (rtt < 0) { + s->fails++; + s->next_ping = now + p->retry_interval; + if (s->fails >= PROBE_FAIL_LIMIT) { s->present = 0; s->rtt = -1; } + } else { + s->fails = 0; + s->next_ping = now + p->probe_interval; + s->present = 1; s->rtt = rtt; + } +} + +/* HTB flags for the families whose presence is known. */ +static void probe_flags(probe_t *p, kvdict_t *d) { + pthread_mutex_lock(&p->mu); + if (p->fam[0].present >= 0) kv_set_int(d, "p4", p->fam[0].present); + if (p->fam[1].present >= 0) kv_set_int(d, "p6", p->fam[1].present); + pthread_mutex_unlock(&p->mu); +} + +/* The `connectivity` metric as a JSON object; returns its length or -1 on overflow. */ +static int probe_metric_json(probe_t *p, char *out, size_t n) { + static const char *names[2] = { "ipv4", "ipv6" }; + size_t len = 0; + len += (size_t)snprintf(out + len, n - len, "{"); + pthread_mutex_lock(&p->mu); + for (int f = 0; f < 2 && len < n; f++) { + const probe_fam_t *s = &p->fam[f]; + char pres[8], addr[64], gw[64], rtt[32]; + snprintf(pres, sizeof(pres), "%s", s->present < 0 ? "null" : s->present ? "true" : "false"); + if (s->addr[0]) snprintf(addr, sizeof(addr), "\"%s\"", s->addr); else snprintf(addr, sizeof(addr), "null"); + if (s->gateway[0]) snprintf(gw, sizeof(gw), "\"%s\"", s->gateway); else snprintf(gw, sizeof(gw), "null"); + if (s->rtt >= 0) snprintf(rtt, sizeof(rtt), "%.3f", s->rtt); else snprintf(rtt, sizeof(rtt), "null"); + len += (size_t)snprintf(out + len, n - len, + "%s\"%s\":{\"present\":%s,\"addr\":%s,\"gateway\":%s,\"rtt\":%s}", + f ? "," : "", names[f], pres, addr, gw, rtt); + } + pthread_mutex_unlock(&p->mu); + if (len + 2 > n) { out[0] = '\0'; return -1; } + out[len++] = '}'; out[len] = '\0'; + 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; } + /* ============================================================ * Plugin: network_monitor * Linux: /proc/net/dev diff --git a/scripts/c/test_probe.c b/scripts/c/test_probe.c new file mode 100644 index 0000000..3d2e570 --- /dev/null +++ b/scripts/c/test_probe.c @@ -0,0 +1,243 @@ +/* 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; +} + +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(); + printf("%d checks, %d failed\n", g_run, g_fail); + return g_fail ? 1 : 0; +}