inet-mon/firmware/main/diagnostics.cpp
Ricardo (XenGi) Band 840909af66
nearly done
2026-07-17 00:08:48 +02:00

638 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file diagnostics.cpp
* @brief Connectivity test suite — ping, DNS, latency, packet loss
*
* Test sequence (DIAG_TEST_COUNT = 8):
* 0. IPv4 config (address + gateway + DNS)
* 1. IPv6 config (address + gateway + DNS)
* 2. Gateway ping (IPv4 + IPv6)
* 3. Uplink ping (IPv4 public IPs)
* 4. DNS resolution (via both DNS servers)
* 5. Latency measurement
* 6. S2S VPN ping
* 7. Temperature
*
* Ping uses raw ICMP sockets (no external dependency needed).
*/
#include "diagnostics.h"
#include "network.h"
#include "temperature.h"
#include "display.h"
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "lwip/icmp.h"
#include "lwip/inet.h"
#include "lwip/inet_chksum.h"
#include "lwip/dns.h"
#include "lwip/ip4_addr.h"
#include "lwip/ip6_addr.h"
#include "lwip/prot/icmp6.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
static const char *TAG = "DIAG";
// Frame counter for spinner animation during pause delays
static int s_diag_frame = 0;
// ─── Test names ─────────────────────────────────────────────────────────────
const char *g_diag_test_names[DIAG_TEST_COUNT] = {
"IPv4",
"IPv6",
"Gateway",
"Uplink",
"DNS",
"Latency",
"S2S VPN",
"Temp"
};
// ─── Public ping targets ────────────────────────────────────────────────────
static const char *s_uplink_v4[4] = {
"1.1.1.1",
"8.8.8.8",
"9.9.9.9",
"208.67.222.222"
};
static const char *s_uplink_v6[4] = {
"2606:4700:4700::1111", // Cloudflare DNS
"2001:4860:4860::8888", // Google DNS
"2620:fe::fe", // Quad9 DNS
"2620:119:35::35" // OpenDNS
};
static const char *s_dns_domains[3] = {
"google.com",
"cloudflare.com",
"example.com"
};
// ─── Raw ICMP Ping ──────────────────────────────────────────────────────────
/// Minimum ICMP packet size (excluding IP header)
#define ICMP_PKT_SIZE 64
/// Calculate IPv4 checksum (used for ICMP header)
static uint16_t icmp_checksum(uint16_t *buf, int len)
{
uint32_t sum = 0;
for (int i = 0; i < len / 2; i++) {
sum += buf[i];
}
if (len & 1) {
sum += ((uint8_t *)buf)[len - 1];
}
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
return (uint16_t)~sum;
}
/// Ping an IPv4 address. Returns average RTT in ms, or -1 on failure.
/// Uses raw ICMP sockets (CONFIG_LWIP_RAW must be enabled).
int diagnostics_ping_ipv4(const char *ip_str, int timeout_ms, int count,
int *out_successes)
{
if (!ip_str || strcmp(ip_str, "--") == 0) {
if (out_successes) *out_successes = 0;
return -1;
}
// Parse IP
struct in_addr addr;
if (!inet_aton(ip_str, &addr)) {
ESP_LOGW(TAG, "Invalid IP: %s", ip_str);
return -1;
}
// Create raw socket
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock < 0) {
ESP_LOGW(TAG, "Cannot create raw socket (errno %d)", errno);
return -1;
}
// Set socket timeout
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_addr = addr;
int rtt_sum = 0;
int responses = 0;
uint16_t pid = (uint16_t)(esp_random() & 0xFFFF);
for (int p = 0; p < count; p++) {
// Build ICMP echo request
uint8_t pkt[ICMP_PKT_SIZE];
struct icmp_echo_hdr *icmp_hdr = (struct icmp_echo_hdr *)pkt;
ICMPH_TYPE_SET(icmp_hdr, ICMP_ECHO);
ICMPH_CODE_SET(icmp_hdr, 0);
icmp_hdr->id = lwip_htons(pid);
icmp_hdr->seqno = lwip_htons(p);
// Fill payload with timestamp (microseconds since boot)
int64_t sent_us = esp_timer_get_time();
memcpy(pkt + sizeof(struct icmp_echo_hdr), &sent_us, sizeof(sent_us));
// Zero out rest of payload (keeps checksum deterministic)
memset(pkt + sizeof(struct icmp_echo_hdr) + sizeof(sent_us), 0,
ICMP_PKT_SIZE - sizeof(struct icmp_echo_hdr) - sizeof(sent_us));
// Zero checksum field, then compute using lwIP's standard function
// NOTE: inet_chksum() returns a value ready to store directly — no htons() needed!
icmp_hdr->chksum = 0;
icmp_hdr->chksum = inet_chksum(pkt, ICMP_PKT_SIZE);
// Send
int ret = sendto(sock, pkt, ICMP_PKT_SIZE, 0,
(struct sockaddr *)&dest, sizeof(dest));
if (ret < 0) {
ESP_LOGW(TAG, "Ping sendto failed: errno %d", errno);
continue;
}
// Receive reply — lwIP raw sockets include the IP header
uint8_t recv_buf[128];
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
ret = recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&from, &from_len);
if (ret >= 0) {
// lwIP delivers data starting at IP header (20 bytes for IPv4)
struct icmp_echo_hdr *reply =
(struct icmp_echo_hdr *)(recv_buf + 20);
if (ICMPH_TYPE(reply) == ICMP_ER &&
reply->id == lwip_htons(pid) &&
reply->seqno == lwip_htons(p)) {
responses++;
// Extract sent timestamp from reply payload
int64_t sent_us_reply;
memcpy(&sent_us_reply,
recv_buf + 20 + sizeof(struct icmp_echo_hdr),
sizeof(sent_us_reply));
int64_t now_us = esp_timer_get_time();
int rtt = (int)((now_us - sent_us_reply) / 1000);
if (rtt < 0) rtt = 0;
rtt_sum += rtt;
}
}
// Update spinner animation after each ping attempt
s_diag_frame++;
display_update_spinner(s_diag_frame);
}
close(sock);
if (out_successes) *out_successes = responses;
if (responses == 0) return -1;
return rtt_sum / responses;
}
/// Ping an IPv6 address. Returns average RTT in ms, or -1 on failure.
int diagnostics_ping_ipv6(const char *ip_str, int timeout_ms, int count,
int *out_successes)
{
if (!ip_str || strcmp(ip_str, "--") == 0) {
if (out_successes) *out_successes = 0;
return -1;
}
// Parse IPv6 address
struct in6_addr addr6;
if (inet_pton(AF_INET6, ip_str, &addr6) != 1) {
ESP_LOGW(TAG, "Invalid IPv6: %s", ip_str);
return -1;
}
// Create raw IPv6 socket
int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
if (sock < 0) {
ESP_LOGW(TAG, "Cannot create IPv6 raw socket (errno %d)", errno);
return -1;
}
// Set socket timeout
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in6 dest;
memset(&dest, 0, sizeof(dest));
dest.sin6_family = AF_INET6;
dest.sin6_addr = addr6;
// Link-local addresses (fe80::/10) need the interface scope ID
if (IN6_IS_ADDR_LINKLOCAL(&addr6)) {
dest.sin6_scope_id = network_get_eth_scope_id();
} else {
dest.sin6_scope_id = 0;
}
int rtt_sum = 0;
int responses = 0;
uint16_t pid = (uint16_t)(esp_random() & 0xFFFF);
for (int p = 0; p < count; p++) {
// Build ICMPv6 echo request
uint8_t pkt[ICMP_PKT_SIZE + 40]; // +40 for IPv6 header if needed
struct icmp6_echo_hdr *icmp6 = (struct icmp6_echo_hdr *)pkt;
icmp6->type = ICMP6_TYPE_EREQ;
icmp6->code = 0;
icmp6->id = lwip_htons(pid);
icmp6->seqno = lwip_htons(p);
// Fill payload with timestamp (microseconds since boot)
int64_t sent_us = esp_timer_get_time();
memcpy(pkt + sizeof(struct icmp6_echo_hdr), &sent_us, sizeof(sent_us));
// Zero out rest of payload
memset(pkt + sizeof(struct icmp6_echo_hdr) + sizeof(sent_us), 0,
ICMP_PKT_SIZE - sizeof(struct icmp6_echo_hdr) - sizeof(sent_us));
// ICMPv6 checksum is computed automatically by lwIP for raw sockets
// (RFC 3542 §3.1 — IPV6_CHECKSUM socket option, cannot be disabled).
// lwIP uses ip6_chksum_pseudo() and overwrites offset 2.
icmp6->chksum = 0;
// Send
int ret = sendto(sock, pkt, ICMP_PKT_SIZE, 0,
(struct sockaddr *)&dest, sizeof(dest));
if (ret < 0) {
ESP_LOGW(TAG, "IPv6 ping sendto failed: %s errno %d", ip_str, errno);
continue;
}
// Receive reply — loop to discard non-echo-reply ICMPv6 traffic (e.g. ND)
uint8_t recv_buf[256];
struct sockaddr_in6 from;
socklen_t from_len;
bool got_reply = false;
for (int tries = 0; tries < 10 && !got_reply; tries++) {
from_len = sizeof(from);
ret = recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&from, &from_len);
if (ret < 0) {
if (tries == 0) {
ESP_LOGI(TAG, "IPv6 recvfrom timeout: %s seq=%d (errno %d)",
ip_str, p, errno);
}
break; // timeout — no more packets
}
if (ret < 40 + (int)sizeof(struct icmp6_echo_hdr)) {
ESP_LOGI(TAG, "IPv6 recv too short: %d bytes", ret);
continue;
}
struct icmp6_echo_hdr *reply =
(struct icmp6_echo_hdr *)(recv_buf + 40);
if (reply->type == ICMP6_TYPE_EREP &&
reply->code == 0 &&
reply->id == lwip_htons(pid) &&
reply->seqno == lwip_htons(p)) {
ESP_LOGI(TAG, "IPv6 ping OK: %s seq=%d", ip_str, p);
responses++;
// Extract sent timestamp from reply payload
int64_t sent_us_reply;
memcpy(&sent_us_reply,
recv_buf + 40 + sizeof(struct icmp6_echo_hdr),
sizeof(sent_us_reply));
int64_t now_us = esp_timer_get_time();
int rtt = (int)((now_us - sent_us_reply) / 1000);
if (rtt < 0) rtt = 0;
rtt_sum += rtt;
got_reply = true;
} else {
ESP_LOGI(TAG, "IPv6 pkt type=%d code=%d id=%d seq=%d",
reply->type, reply->code, reply->id, reply->seqno);
}
}
// Update spinner animation after each IPv6 ping attempt
s_diag_frame++;
display_update_spinner(s_diag_frame);
}
close(sock);
if (out_successes) *out_successes = responses;
if (responses == 0) return -1;
return rtt_sum / responses;
}
// ─── DNS resolution ─────────────────────────────────────────────────────────
bool diagnostics_dns_lookup(const char *hostname, const char **dns_result,
int timeout_ms)
{
if (!hostname) return false;
struct addrinfo hints;
struct addrinfo *result = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // both A (IPv4) and AAAA (IPv6) records
hints.ai_socktype = SOCK_STREAM;
int ret = getaddrinfo(hostname, NULL, &hints, &result);
if (ret == 0 && result) {
freeaddrinfo(result);
if (dns_result) *dns_result = "ok";
return true;
}
ESP_LOGW(TAG, "DNS resolve failed for '%s': ret=%d", hostname, ret);
if (dns_result) {
*dns_result = (ret == EAI_AGAIN) ? "timeout" : "nxdomain";
}
return false;
}
// ─── DNS via specific server ─────────────────────────────────────────────
/// Try DNS resolution via a specific DNS server IP.
/// Temporarily sets the lwIP DNS servers, queries all domains,
/// then restores the original DNS config.
/// Returns true if any domain returned any record (A or AAAA).
static bool test_dns_via_server(const char *dns_str,
const char *domains[], int num_domains,
int timeout_ms)
{
if (!dns_str || strcmp(dns_str, "--") == 0) return false;
// Save current DNS config
ip_addr_t saved[DNS_MAX_SERVERS];
for (int i = 0; i < DNS_MAX_SERVERS; i++) {
saved[i] = *dns_getserver(i); // dereference returned pointer
}
// Parse and set the target DNS server as the sole resolver
ip_addr_t target = IPADDR4_INIT(0);
ip4_addr_t ip4;
if (ip4addr_aton(dns_str, &ip4)) {
ip_addr_copy_from_ip4(target, ip4);
} else {
ip6_addr_t ip6;
if (ip6addr_aton(dns_str, &ip6)) {
ip_addr_copy_from_ip6(target, ip6);
} else {
// Invalid address — restore and bail
for (int i = 0; i < DNS_MAX_SERVERS; i++) {
dns_setserver(i, &saved[i]);
}
return false;
}
}
dns_setserver(0, &target);
for (int i = 1; i < DNS_MAX_SERVERS; i++) {
dns_setserver(i, IP_ADDR_ANY);
}
// Try each domain
bool any_ok = false;
for (int i = 0; i < num_domains; i++) {
const char *dns_status = NULL;
if (diagnostics_dns_lookup(domains[i], &dns_status, timeout_ms)) {
any_ok = true;
break;
}
}
// Restore original DNS config
for (int i = 0; i < DNS_MAX_SERVERS; i++) {
dns_setserver(i, &saved[i]);
}
return any_ok;
}
// ─── Latency measurement ────────────────────────────────────────────────────
static void measure_latency_and_loss(int timeout_ms,
int *out_avg_rtt,
float *out_loss_pct,
bool *out_v4_ok,
bool *out_v6_ok)
{
int total_rtt = 0;
int successes = 0;
bool any_v4 = false;
bool any_v6 = false;
int total = 0;
// Ping all 4 IPv4 targets (5 pings each for accurate loss)
for (int i = 0; i < 4; i++) {
int pkts = 0;
int rtt = diagnostics_ping_ipv4(s_uplink_v4[i], 1000, 5, &pkts);
successes += pkts;
if (rtt >= 0) {
total_rtt += rtt * pkts;
any_v4 = true;
ESP_LOGI(TAG, "Ping %s: RTT=%dms (%d/%d)", s_uplink_v4[i], rtt, pkts, 5);
} else {
ESP_LOGW(TAG, "Ping %s: FAILED (0/%d)", s_uplink_v4[i], 5);
}
}
// Ping all 4 IPv6 targets (5 pings each for accurate loss)
for (int i = 0; i < 4; i++) {
int pkts = 0;
int rtt = diagnostics_ping_ipv6(s_uplink_v6[i], 1000, 5, &pkts);
successes += pkts;
if (rtt >= 0) {
total_rtt += rtt * pkts;
any_v6 = true;
ESP_LOGI(TAG, "Ping %s: RTT=%dms (%d/%d)", s_uplink_v6[i], rtt, pkts, 5);
} else {
ESP_LOGW(TAG, "Ping %s: FAILED (0/%d)", s_uplink_v6[i], 5);
}
}
total = 40; // 8 targets × 5 pings each
if (out_v4_ok) *out_v4_ok = any_v4;
if (out_v6_ok) *out_v6_ok = any_v6;
if (out_loss_pct) {
if (total > 0) {
*out_loss_pct = ((float)(total - successes) / (float)total) * 100.0f;
} else {
*out_loss_pct = -1.0f;
}
}
if (out_avg_rtt) {
if (successes > 0) {
*out_avg_rtt = total_rtt / successes;
ESP_LOGI(TAG, "Latency & Packet Loss: avg RTT=%dms, loss=%.0f%% (%d/%d pings)",
*out_avg_rtt, *out_loss_pct, successes, total);
} else {
*out_avg_rtt = -1;
}
}
}
// ─── Run all tests ──────────────────────────────────────────────────────────
// Helper: pause 1 second so user can read the progress display
// Pause ~1 second with spinner animation (calls display_update_spinner each 100ms)
static void diag_pause(void)
{
for (int i = 0; i < 10; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
s_diag_frame++;
display_update_spinner(s_diag_frame);
}
}
void diagnostics_run_all(struct test_results_t *results,
int timeout_ms,
diag_progress_cb_t progress_cb)
{
if (!results) return;
results->tests_completed = 0;
results->tests_total = DIAG_TEST_COUNT;
// ── Test 0: IPv4 config (address + gateway + DNS via DHCP) ──
diag_pause();
results->has_ipv4 = network_has_ipv4();
results->ipv4 = results->has_ipv4 ? network_get_ipv4_str() : "--";
results->gateway_ip = results->has_ipv4 ? network_get_gateway_str() : "--";
const char *dns4 = network_get_dns_str();
bool has_gw = results->has_ipv4 && results->gateway_ip && strcmp(results->gateway_ip, "--") != 0;
bool has_dns = dns4 && strcmp(dns4, "--") != 0;
ESP_LOGI(TAG, "IPv4: %s gw=%s dns=%s",
results->ipv4,
has_gw ? results->gateway_ip : "--",
has_dns ? dns4 : "--");
results->tests_completed = 1;
if (progress_cb) progress_cb(results, 0);
// ── Test 1: IPv6 config (address + gateway + DNS via SLAAC/RDNSS) ──
diag_pause();
results->has_ipv6 = network_has_ipv6();
results->ipv6 = results->has_ipv6 ? network_get_ipv6_str() : "--";
results->gateway6_ip = results->has_ipv6 ? network_get_gateway6_str() : "--";
const char *dns6 = network_get_dns6_str();
bool has_gw6 = results->has_ipv6 && results->gateway6_ip && strcmp(results->gateway6_ip, "--") != 0;
bool has_dns6 = dns6 && strcmp(dns6, "--") != 0;
ESP_LOGI(TAG, "IPv6: %s gw=%s dns=%s",
results->ipv6,
has_gw6 ? results->gateway6_ip : "--",
has_dns6 ? dns6 : "--");
results->tests_completed = 2;
if (progress_cb) progress_cb(results, 1);
// ── Test 2: Gateway ping (IPv4 + IPv6) ──
diag_pause();
const char *gw4 = network_get_gateway_str();
results->gateway_reachable_v4 = (diagnostics_ping_ipv4(gw4, timeout_ms, 3, NULL) >= 0);
ESP_LOGI(TAG, "Gateway %s: %s", gw4,
results->gateway_reachable_v4 ? "OK" : "FAIL");
const char *gw6 = results->gateway6_ip;
bool can_ping_gw6 = gw6 && strcmp(gw6, "--") != 0;
results->gateway_reachable_v6 = (can_ping_gw6 && diagnostics_ping_ipv6(gw6, timeout_ms, 3, NULL) >= 0);
if (can_ping_gw6) {
ESP_LOGI(TAG, "Gateway6 %s: %s", gw6,
results->gateway_reachable_v6 ? "OK" : "FAIL");
}
results->tests_completed = 3;
if (progress_cb) progress_cb(results, 2);
// ── Test 3: Uplink ping (IPv4 + IPv6) ──
diag_pause();
results->uplink_reachable_v4 = false;
for (int i = 0; i < 4; i++) {
if (diagnostics_ping_ipv4(s_uplink_v4[i], timeout_ms, 1, NULL) >= 0) {
results->uplink_reachable_v4 = true;
break;
}
}
results->uplink_reachable_v6 = false;
for (int i = 0; i < 4; i++) {
if (diagnostics_ping_ipv6(s_uplink_v6[i], timeout_ms, 1, NULL) >= 0) {
results->uplink_reachable_v6 = true;
break;
}
}
ESP_LOGI(TAG, "Uplink: v4=%s v6=%s",
results->uplink_reachable_v4 ? "OK" : "FAIL",
results->uplink_reachable_v6 ? "OK" : "FAIL");
results->tests_completed = 4;
if (progress_cb) progress_cb(results, 3);
// ── Test 4: DNS resolution ──
diag_pause();
results->dns4_ok = false;
results->dns6_ok = false;
results->dns_timeout = false;
// Test via IPv4 DNS server (from DHCP)
results->dns4_ok = test_dns_via_server(
network_get_dns_str(), s_dns_domains, 3, timeout_ms);
// Test via IPv6 DNS server (from RDNSS)
results->dns6_ok = test_dns_via_server(
network_get_dns6_str(), s_dns_domains, 3, timeout_ms);
results->dns_ok = results->dns4_ok || results->dns6_ok;
if (!results->dns_ok) {
results->dns_timeout = true;
}
ESP_LOGI(TAG, "DNS: %s (v4=%s v6=%s)",
results->dns_ok ? "OK" : "FAIL",
results->dns4_ok ? "OK" : "FAIL",
results->dns6_ok ? "OK" : "FAIL");
results->tests_completed = 5;
if (progress_cb) progress_cb(results, 4);
// ── Test 5: Latency & packet loss ──
diag_pause();
float loss = -1.0f;
int avg_rtt = -1;
bool lat_v4_ok = false, lat_v6_ok = false;
measure_latency_and_loss(timeout_ms, &avg_rtt, &loss, &lat_v4_ok, &lat_v6_ok);
results->latency_v4_ok = lat_v4_ok;
results->latency_v6_ok = lat_v6_ok;
results->latency_ms = avg_rtt;
results->packet_loss_pct = loss;
results->tests_completed = 6;
if (progress_cb) progress_cb(results, 5);
// ── Test 6: RS25 S2S VPN ping (1.1.1.1 placeholder, detects VPN forwarding) ──
diag_pause();
results->vpn_reachable = (diagnostics_ping_ipv4("1.1.1.1", timeout_ms, 2, NULL) >= 0);
ESP_LOGI(TAG, "RS25 VPN: %s",
results->vpn_reachable ? "OK" : "FAIL");
results->tests_completed = 7;
if (progress_cb) progress_cb(results, 6);
// ── Test 7: Temperature ──
diag_pause();
results->temperature_c = temperature_read();
ESP_LOGI(TAG, "Temperature: %.1f C", results->temperature_c);
results->tests_completed = 8;
if (progress_cb) progress_cb(results, 7);
ESP_LOGI(TAG, "All tests completed (%d/%d)",
results->tests_completed, results->tests_total);
}