Add Dual-Stack Network Diagnostics

This commit is contained in:
2026-09-21 10:01:55 +02:00
parent 60c1e279d6
commit 06df47c934
18 changed files with 1488 additions and 270 deletions
+160
View File
@@ -0,0 +1,160 @@
# Shared IPv4/IPv6 diagnostics fixtures and integration handoff
## Commands
```text
ping [-4|-6] <host> [count] count 1..20, default 4
traceroute [-4|-6] <host> [max-hops] hops 1..30, default 16
nslookup [-4|-6] <host>
```
One family flag may occur anywhere after the command, including after the host or
numeric argument. Repeated/conflicting flags, unknown options, empty arguments,
extra arguments, and non-decimal/out-of-range counts are rejected. Existing bare
commands and aliases calling `network_console_execute()` share these semantics.
For example: `ping fe80::1%sta -6 3`, `traceroute example.net 20 -6`.
Default hostname probes resolve IPv4 first, then IPv6 only when resolution returns
no address (`EAI_FAIL`/`EAI_NONAME`). They **never** retry another family after a
send failure, timeout, or unreachable reply. This is not Happy Eyeballs. Explicit
`-6` neither requests nor accepts IPv4/mapped IPv4. Default hostname `nslookup`
makes separate A and AAAA queries, prints partial success, and explains resolver
limits. Numeric literals do not query DNS or perform reverse lookup.
Zones refer to interfaces **on the ESP32**, not on the browser/SSH client:
`%sta`, `%ap`, a currently valid numeric lwIP index (1..255), or an existing lwIP
interface name. Ping/traceroute reject unscoped link-local addresses (including
DNS AAAA answers with zone zero), showing the address and scoped-literal examples;
no default interface is guessed. `nslookup` displays those records and unscoped
link-local literals with a note that probes require an explicit device scope.
Mapped IPv4 literals remain rejected. Scoped ping preserves `sin6_scope_id` as the `ip6_addr_t` zone **and** sets
`esp_ping_config_t.interface`; scoped traceroute retains the socket zone and binds
the raw socket to that interface. Numeric output includes the zone.
## Run
```sh
python3 tests/network_diagnostics/run.py
python3 tests/network_diagnostics/run.py --sanitize # optional installed ASan/UBSan
python3 tests/network_diagnostics/sdk_contract.py /path/to/framework-espidf
```
`run.py` includes the actual production `src/network_console.c`, not a duplicate
parser/implementation. SDK calls and socket operations are deterministic fakes;
there is no networking or hardware access. It checks argument permutations and
bounds, per-family resolution/fallback/freeing, A+AAAA queries, scope parsing and
ping zone/interface conversion, valid echo/time-exceeded/unreachable packets,
all truncation lengths, bad checksums/IDs/sequences/targets/codes/protocols,
unsupported IPv6 extension headers, recv source correlation, absolute deadlines,
1/500/999/1000-us remaining-budget boundaries and unrelated traffic leaving 500 us,
zero-zone link-local AAAA display versus rejection before any probe socket/session,
EINTR, all configured socket-option failure points, send/receive/create errors,
fd-zero cleanup, PSRAM allocation failure, and ping create/start/delayed-completion
lifetime handling. Also runs 10,000 deterministic malformed packet inputs.
## Audited IDF 5.5.0 / lwIP contract
`sdk_contract.py` pins seven installed source files, including the FreeRTOS mailbox
port. It also extracts and compiles the actual SDK timeval conversion macro and
`sys_arch_mbox_fetch()` with a fake queue boundary, checking the infinite-wait
sentinel for sub-millisecond values and finite waits for >=1 ms at 1/10-ms ticks.
The production module also has an IDF version guard; upgrades require re-audit.
- `api/netdb.c`: `AF_INET` and `AF_INET6` choose explicit DNS address types. Current
generated `CONFIG_LWIP_DNS_MAX_HOST_IP=1` means one returned address per query;
this command prints one selected address per family, not a complete RRset.
DNS errors collapse into `EAI_FAIL`; NXDOMAIN, timeout, and server failure cannot
reliably be distinguished. Resolution itself uses SDK DNS timeouts, not a
command-owned deadline. `.local` depends on lwIP mDNS-query support, not client NSS.
- `core/ipv6/ip6.c` restores the complete IPv6 header before `raw_input()`;
`api/api_msg.c:recv_raw()` copies it. Raw receives are **not Linux's ICMPv6-only
framing**. Raw delivery precedes ICMP checksum validation, so traceroute checks
the complete outer ICMP checksum (including IPv6 pseudoheader) itself.
- `core/raw.c` demultiplexes the IPv6 **base** next-header. Therefore traceroute
explicitly rejects all outer/quoted IPv6 extension headers, including fragment,
routing, hop-by-hop, destination options, AH and ESP. It does not walk or guess
unsupported forms. Quoted packets may be incomplete after the required echo
header, but the complete outer packet must fit/be received and pass its checksum.
- `api/api_msg.c` enables checksum generation at offset 2 for raw ICMPv6 sockets.
`api/sockets.c` refuses changing `IPV6_CHECKSUM` for ICMPv6; do not set it.
`IPPROTO_IP/IP_TTL` sets the common PCB TTL used for **both** IPv4 TTL and IPv6
hop limit. `IPV6_UNICAST_HOPS` is not implemented. Fixtures require the actual
supported option for both families. IPv6 traceroute also sets `IPV6_V6ONLY`.
SDK ping's `IPPROTO_IP/IP_TTL` use is likewise correct for IPv6; it is unchanged.
- `api/sockets.c` floors `SO_RCVTIMEO` timeval values to milliseconds;
`port/freertos/sys_arch.c:sys_arch_mbox_fetch()` interprets zero milliseconds
as `portMAX_DELAY`, not a poll. Traceroute stops when <1000 us remain, never
supplying a zero-millisecond receive timeout.
- `apps/ping/ping_sock.c` does not copy the target zone into `sin6_scope_id`;
`config.interface` uses `SO_BINDTODEVICE` and is required for scoped ping.
SDK ping is not the strict traceroute parser: its receive validation and IPv6
profile formatting remain upstream behavior (reply profiles do not retain the
interface zone). Do not infer traceroute's packet correlation guarantees for
SDK ping.
## Bounds and remaining SDK limitation
Traceroute owns one raw socket in the dispatcher, sends one eight-byte probe per
hop, and has a one-second **absolute receive deadline** per hop (unrelated packets
and EINTR cannot extend it). It may stop up to 999 us early to avoid lwIP's
zero-millisecond infinite-wait sentinel. Every post-creation exit closes it. The 1280-byte
receive buffer rejects larger/truncated outer replies rather than partially
matching them. DNS, scheduler and synchronous send latency are not included in
that deadline. No extra task, serial hot-path work, or persistent trace allocation.
Ping keeps the existing 21-event, **4200-byte PSRAM-only**, lazily retained queue
payload and SDK's transient task. Count <=20, timeout=1000ms, interval=1000ms.
The console wait has an absolute `count * 2000 + 2000` ms budget after start.
Callback context is now permanent (not a caller-stack pointer); if that deadline
expires, the callback queue remains reserved until its END event. Until then,
later ping calls fail busy rather than overlapping callback-producing sessions or
resetting their queue. **END ends callback production, not SDK task/socket
retirement:** `esp_ping_delete_session()` only requests asynchronous deletion.
A new command may start while the previous task/socket is briefly retiring; this
is not a guarantee that two SDK tasks/sockets can never coexist.
**Hard ping socket-lifetime bounds are not available from this SDK API:** unrelated
raw ICMP traffic can repeatedly refresh `esp_ping_receive()`'s receive timeout;
`stop`/`delete` are asynchronous and cannot interrupt that loop. The timeout path
must not touch a potentially concurrently deleted handle. The retained reservation
is intentional failure isolation, not proof of synchronous cleanup. A strict hard
socket deadline would require replacing SDK ping with a dispatcher-owned raw ping
implementation or an SDK cancellation helper with an acknowledged lifetime
contract. No dependency/SDK patch was made. This limitation is for the parent to
assess; ordinary SDK completion still self-deletes and releases its transient task.
## Validation and measured impact (2026-09-21)
- Host production fixtures: **8027 checks + 10,000 fuzz inputs PASS**.
- Installed seven-file SDK hash contract and actual timeout conversion/mailbox fixtures: PASS.
- Actual target compiler: syntax-only PASS after the review fixes. Earlier isolated
baseline/current object compiles used the existing `network_console.c` compilation
arguments. No `pio` build, project build artifact writes, upload, monitor, or
hardware test.
- Pre-review object measurement (not remeasured after the timeout/scope fixes):
`text`: 7281 -> 9917 bytes (**+2636**); `bss`: 92 -> 97 (**+5**);
`data`: unchanged 0. These are object measurements, **not final linked firmware**.
- Pre-review target `-fstack-usage` frames: `execute_ping` 400 -> 464; `execute_nslookup`
128 -> 144; `execute_traceroute` 224 -> 256; `wait_for_trace_reply` 240 -> 1408.
These individual frames are not a measured runtime stack high-water mark.
- ASan/UBSan attempt could not link: host `libasan.so.8.0.0` and
`libubsan.so.1.0.0` are missing. Ordinary `-Wall -Wextra -Werror` fixtures passed.
## Integrated surface and build validation
Wi-Fi help and root/alias completion use the syntax above. The production console
boundary is covered by `python3 tests/network_diagnostics_surfaces/run.py`:
432 SSH/browser root-and-alias cases, UART0 parity, role/revocation guards, and
fixed-token completion. All surfaces forward to the same canonical handler.
No new ESP-NETIF ownership helper is needed: public
`esp_netif_get_handle_from_ifkey()` / `esp_netif_get_netif_impl_index()` map aliases.
IPv4+IPv6, raw sockets and IPv6 scopes must remain enabled. The installed
configuration supplies these; no configuration/dependency changes were made.
Final `pio run` passed: **94,452 B linked RAM / 1,857,673 B flash**
(+8 B / +3,188 B versus the recorded pre-follow-up build). This is not runtime
heap or stack headroom. Host fixtures, exact SDK contract, surface tests and
existing admin policy/Wi-Fi prompt regressions passed. No upload, packet capture,
live DNS, multi-hop trace or hardware validation was performed. The prior Phase12
acceptance must not be treated as acceptance of this later diagnostics change.
+98
View File
@@ -0,0 +1,98 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#pragma once
#include <assert.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <net/if.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#define CONFIG_LWIP_IPV6 1
#define CONFIG_LWIP_DNS_MAX_HOST_IP 1
#define ESP_IDF_VERSION_VAL(a,b,c) (((a)<<16)|((b)<<8)|(c))
#define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(5,5,0)
typedef int esp_err_t;
#define ESP_OK 0
#define MALLOC_CAP_SPIRAM 1
#define MALLOC_CAP_8BIT 2
#define pdTRUE 1
#define portMAX_DELAY UINT32_MAX
#define pdMS_TO_TICKS(x) (x)
typedef uint32_t TickType_t;
typedef uint16_t u16_t;
typedef struct { int unused; } StaticQueue_t;
typedef void *QueueHandle_t;
typedef struct { struct in6_addr addr; uint8_t zone; } ip6_addr_t;
typedef struct in_addr ip4_addr_t;
typedef struct { int family; union { ip4_addr_t v4; ip6_addr_t v6; }; } ip_addr_t;
#define inet_addr_to_ip4addr(d,s) (*(d)=*(s))
#define inet6_addr_to_ip6addr(d,s) ((d)->addr=*(s))
#define ip6_addr_set_zone(d,z) ((d)->zone=(z))
#define ip_addr_copy_from_ip4(d,s) ((d).family=AF_INET,(d).v4=(s))
#define ip_addr_copy_from_ip6(d,s) ((d).family=AF_INET6,(d).v6=(s))
#define IP_IS_V4(d) ((d)->family==AF_INET)
#define lwip_htons htons
#define ICMP_ECHO 8
#define ICMP_ER 0
#define ICMP_TE 11
#define ICMP_DUR 3
struct icmp_echo_hdr { uint8_t type, code; uint16_t chksum, id, seqno; };
typedef void *esp_ping_handle_t;
typedef struct { uint32_t count, interface, interval_ms, timeout_ms; ip_addr_t target_addr; } esp_ping_config_t;
#define ESP_PING_DEFAULT_CONFIG() ((esp_ping_config_t){0})
typedef struct {
void *cb_args;
void (*on_ping_success)(esp_ping_handle_t,void *);
void (*on_ping_timeout)(esp_ping_handle_t,void *);
void (*on_ping_end)(esp_ping_handle_t,void *);
} esp_ping_callbacks_t;
enum { ESP_PING_PROF_SEQNO, ESP_PING_PROF_SIZE, ESP_PING_PROF_TIMEGAP,
ESP_PING_PROF_IPADDR, ESP_PING_PROF_TTL, ESP_PING_PROF_REQUEST,
ESP_PING_PROF_REPLY, ESP_PING_PROF_DURATION };
typedef struct { const char *command, *help, *hint; int (*func)(int,char **); void *argtable; } esp_console_cmd_t;
typedef int esp_netif_t;
int fake_getaddrinfo(const char *, const char *, const struct addrinfo *, struct addrinfo **);
void fake_freeaddrinfo(struct addrinfo *);
int fake_socket(int,int,int);
int fake_setsockopt(int,int,int,const void *,socklen_t);
ssize_t fake_sendto(int,const void *,size_t,int,const struct sockaddr *,socklen_t);
ssize_t fake_recvfrom(int,void *,size_t,int,struct sockaddr *,socklen_t *);
int fake_close(int);
unsigned int fake_if_nametoindex(const char *);
char *fake_if_indextoname(unsigned int,char *);
#define getaddrinfo fake_getaddrinfo
#define freeaddrinfo fake_freeaddrinfo
#define socket fake_socket
#define setsockopt fake_setsockopt
#define sendto fake_sendto
#define recvfrom fake_recvfrom
#define close fake_close
#define if_nametoindex fake_if_nametoindex
#define if_indextoname fake_if_indextoname
size_t strlcpy(char *,const char *,size_t);
const char *esp_err_to_name(esp_err_t);
void *heap_caps_malloc(size_t,int);
QueueHandle_t xQueueCreateStatic(unsigned int,unsigned int,uint8_t *,StaticQueue_t *);
int xQueueReset(QueueHandle_t);
int xQueueSend(QueueHandle_t,const void *,unsigned int);
int xQueueReceive(QueueHandle_t,void *,unsigned int);
const char *ipaddr_ntoa_r(const ip_addr_t *,char *,int);
esp_err_t esp_ping_get_profile(esp_ping_handle_t,int,void *,size_t);
esp_err_t esp_ping_new_session(const esp_ping_config_t *,const esp_ping_callbacks_t *,esp_ping_handle_t *);
esp_err_t esp_ping_start(esp_ping_handle_t);
esp_err_t esp_ping_delete_session(esp_ping_handle_t);
esp_err_t esp_ping_stop(esp_ping_handle_t);
int64_t esp_timer_get_time(void);
uint16_t inet_chksum(const void *,u16_t);
esp_netif_t *esp_netif_get_handle_from_ifkey(const char *);
int esp_netif_get_netif_impl_index(esp_netif_t *);
esp_err_t esp_console_cmd_register(const esp_console_cmd_t *);
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Compile the actual network_console.c with deterministic boundary fakes; no networking."""
import argparse
import pathlib
import subprocess
import tempfile
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sanitize", action="store_true", help="enable ASan and UBSan if installed")
options = parser.parse_args()
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parents[1]
headers = [
"esp_console.h", "esp_err.h", "esp_heap_caps.h", "esp_netif.h",
"esp_idf_version.h", "esp_timer.h", "freertos/FreeRTOS.h",
"freertos/queue.h", "freertos/task.h", "lwip/inet.h", "lwip/inet_chksum.h",
"lwip/ip_addr.h", "lwip/netdb.h", "lwip/netif.h", "lwip/prot/icmp.h",
"lwip/prot/ip4.h", "lwip/sockets.h", "ping/ping_sock.h",
]
with tempfile.TemporaryDirectory(prefix="network-diagnostics-") as directory:
tmp = pathlib.Path(directory)
for header in headers:
path = tmp / header
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('#include "fakes.h"\n')
subprocess.run([
"cc", "-std=c11", "-D_DEFAULT_SOURCE", "-Wall", "-Wextra", "-Werror",
"-g", "-O1", *(["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] if options.sanitize else []),
"-I", str(tmp), "-I", str(HERE),
str(HERE / "test.c"), "-o", str(tmp / "test"),
], cwd=ROOT, check=True)
subprocess.run([str(tmp / "test")], cwd=ROOT, check=True)
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Fail on drift from the IDF 5.5.0 sources reviewed for raw socket diagnostics."""
import argparse
import hashlib
import re
import subprocess
import tempfile
from pathlib import Path
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("idf", type=Path, help="installed framework-espidf directory")
args = parser.parse_args()
assert (args.idf / "version.txt").read_text().strip() == "5.5.0"
hashes = {
"apps/ping/ping_sock.c": "39b4be760a2137681f9f07d077c69b8de0cc1d88bed79ccfafaa1814d1d68fa5",
"lwip/src/api/api_msg.c": "a793b371032ba38ca5e23fd7fede4c910aea0079ffe612d781109f203eab92a0",
"lwip/src/api/sockets.c": "861822d05ac60f68228279036a966c26acb1282b8f2f7bd0c4672129c7fd74a4",
"lwip/src/api/netdb.c": "9df1796dbd6506d52776d67f6145199a1de36cf3e8eeb657ec9230573766af0c",
"lwip/src/core/raw.c": "3ba4f2e5d2b61bbfbe813653c7f4e88e2542d567808d3f03be000e0876ce0392",
"lwip/src/core/ipv6/ip6.c": "d48495404ceea84fdc97677deff00a463b2dcee59f7ead3a7661c26344e9cea0",
"port/freertos/sys_arch.c": "304978233ec040c5f0e95e1a08209c038b5e03695638e1640301fa1f07c307b2",
}
for relative, expected in hashes.items():
path = args.idf / "components/lwip" / relative
actual = hashlib.sha256(path.read_bytes()).hexdigest()
assert actual == expected, f"Re-audit {path}: expected {expected}, got {actual}"
# Compile the installed conversion macro AND mailbox implementation, not copies.
# The queue boundary is fake so testing the infinite-wait sentinel never blocks.
sockets = (args.idf / "components/lwip/lwip/src/api/sockets.c").read_text()
conversion = next(line for line in sockets.splitlines()
if line.startswith("#define LWIP_SO_SNDRCVTIMEO_GET_MS")
and "struct timeval" in line)
sys_arch = (args.idf / "components/lwip/port/freertos/sys_arch.c").read_text()
mailbox = re.search(r"u32_t\nsys_arch_mbox_fetch\([^\n]*\n\{.*?\n\}", sys_arch, re.S)
assert mailbox is not None
fixture = r'''
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <sys/time.h>
typedef uint32_t u32_t;
typedef uint32_t TickType_t;
typedef int BaseType_t;
typedef struct { void *os_mbox; } fake_mbox_t;
typedef fake_mbox_t *sys_mbox_t;
#define portMAX_DELAY UINT32_MAX
#define pdTRUE 1
#define errQUEUE_EMPTY 0
#define SYS_ARCH_TIMEOUT UINT32_MAX
#define LWIP_ASSERT(message, condition) assert(condition)
static TickType_t observed_ticks;
static BaseType_t xQueueReceive(void *queue, void *msg, TickType_t ticks)
{
(void)queue; (void)msg;
observed_ticks=ticks;
return pdTRUE;
}
'''
fixture += conversion + "\n" + mailbox.group() + r'''
int main(void)
{
fake_mbox_t storage={0};
sys_mbox_t box=&storage;
const long microseconds[]={0,1,500,999,1000,1500,999999};
for (unsigned i=0;i<sizeof(microseconds)/sizeof(microseconds[0]);++i) {
struct timeval value={.tv_usec=microseconds[i]};
long ms=LWIP_SO_SNDRCVTIMEO_GET_MS(&value);
assert(ms==microseconds[i]/1000);
assert(sys_arch_mbox_fetch(&box,NULL,(u32_t)ms)==0);
if (microseconds[i]<1000) {
assert(ms==0 && observed_ticks==portMAX_DELAY);
} else {
assert(ms>0 && observed_ticks!=portMAX_DELAY);
assert(observed_ticks==(u32_t)ms/portTICK_PERIOD_MS);
}
}
return 0;
}
'''
with tempfile.TemporaryDirectory(prefix="network-sdk-timeout-") as directory:
path = Path(directory)
source = path / "timeout.c"
source.write_text(fixture)
for tick_ms in (1, 10):
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
f"-DportTICK_PERIOD_MS={tick_ms}", str(source),
"-o", str(path / "timeout")], check=True)
subprocess.run([str(path / "timeout")], check=True)
print("IDF 5.5.0 network diagnostics: 7 hashes + actual timeout conversion/mailbox fixtures PASS")
+383
View File
@@ -0,0 +1,383 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include "../../src/network_console.c"
#undef close /* Output-capture descriptors are real host files, not fake sockets. */
static unsigned checks;
#define CHECK(x) do { ++checks; if (!(x)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#x); abort(); } } while (0)
static int queries[8], query_count, resolver_error4, resolver_error6, freed;
static bool resolver_linklocal;
static long socket_timeout_ms;
static unsigned recv_calls, ping_created;
static struct addrinfo ai;
static struct sockaddr_storage answer;
static int sockets, closed, options, fail_option, send_fail, recv_error, socket_fail;
static int socket_family, socket_protocol, sent_count, ttl_options, v6only_options, bind_options;
static uint8_t incoming[1280];
static size_t incoming_length;
static struct sockaddr_storage incoming_source;
static int64_t now;
static esp_ping_config_t ping_config;
static esp_ping_callbacks_t ping_callbacks;
static int ping_create_error, ping_start_error, ping_deleted;
static bool ping_delayed;
static uint8_t *queue_bytes;
static unsigned queue_size, queue_read, queue_write;
static bool allocation_fail;
static void reset(void)
{
query_count = freed = resolver_error4 = resolver_error6 = 0;
resolver_linklocal = false;
socket_timeout_ms = 0;
recv_calls = ping_created = 0;
sockets = closed = options = fail_option = send_fail = recv_error = socket_fail = 0;
sent_count = ttl_options = v6only_options = bind_options = 0;
incoming_length = 0;
now = 0;
ping_create_error = ping_start_error = ping_deleted = 0;
ping_delayed = false;
allocation_fail = false;
}
int fake_getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **out)
{
(void)host; CHECK(service == NULL); CHECK(query_count < 8);
queries[query_count++] = hints->ai_family;
CHECK(hints->ai_flags == 0);
int error = hints->ai_family == AF_INET ? resolver_error4 : resolver_error6;
*out = NULL;
if (error) return error;
memset(&answer,0,sizeof(answer));
if (hints->ai_family == AF_INET) {
struct sockaddr_in *v4 = (void *)&answer;
v4->sin_family = AF_INET;
inet_pton(AF_INET,"192.0.2.1",&v4->sin_addr);
} else {
struct sockaddr_in6 *v6 = (void *)&answer;
v6->sin6_family = AF_INET6;
inet_pton(AF_INET6,resolver_linklocal?"fe80::1":"2001:db8::1",&v6->sin6_addr);
CHECK(v6->sin6_scope_id==0); /* DNS does not supply an interface zone. */
}
ai = (struct addrinfo){.ai_family=hints->ai_family,.ai_addr=(void *)&answer,.ai_addrlen=sizeof(answer)};
*out = &ai;
return 0;
}
void fake_freeaddrinfo(struct addrinfo *p) { CHECK(p == &ai); ++freed; }
int fake_socket(int family,int type,int protocol)
{
CHECK(type == SOCK_RAW); socket_family=family; socket_protocol=protocol;
if (socket_fail) { errno=ENOMEM; return -1; }
++sockets; return 0; /* fd zero is valid and must be closed */
}
int fake_setsockopt(int fd,int level,int option,const void *value,socklen_t length)
{
CHECK(fd==0); ++options;
if (fail_option==options) { errno=EINVAL; return -1; }
if (level==IPPROTO_IP && option==IP_TTL) {
CHECK(length==sizeof(int)); CHECK(*(const int *)value>=1 && *(const int *)value<=30); ++ttl_options;
} else if (level==IPPROTO_IPV6 && option==IPV6_V6ONLY) {
CHECK(*(const int *)value==1); ++v6only_options;
} else if (level==SOL_SOCKET && option==SO_BINDTODEVICE) {
CHECK(length==sizeof(struct ifreq)); CHECK(strcmp(((const struct ifreq *)value)->ifr_name,"st1")==0); ++bind_options;
} else {
CHECK(level==SOL_SOCKET && option==SO_RCVTIMEO);
CHECK(length==sizeof(struct timeval));
const struct timeval *timeout=value;
CHECK(timeout->tv_sec<=1 && timeout->tv_usec<1000000);
/* sdk_contract.py also compiles the installed SDK's actual conversion. */
socket_timeout_ms=timeout->tv_sec*1000+timeout->tv_usec/1000;
CHECK(socket_timeout_ms>0);
}
return 0;
}
ssize_t fake_sendto(int fd,const void *data,size_t length,int flags,const struct sockaddr *address,socklen_t address_length)
{
(void)flags; CHECK(fd==0); CHECK(length==8); ++sent_count;
CHECK(address->sa_family==socket_family);
const struct icmp_echo_hdr *echo=data;
if (socket_family==AF_INET6) {
CHECK(socket_protocol==IPPROTO_ICMPV6); CHECK(echo->type==128); CHECK(echo->chksum==0);
CHECK(address_length==sizeof(struct sockaddr_in6));
} else {
CHECK(socket_protocol==IPPROTO_ICMP); CHECK(echo->type==ICMP_ECHO); CHECK(inet_chksum(data,length)==0);
CHECK(address_length==sizeof(struct sockaddr_in));
}
if (send_fail) { errno=ENETUNREACH; return -1; }
return length;
}
ssize_t fake_recvfrom(int fd,void *data,size_t length,int flags,struct sockaddr *source,socklen_t *source_length)
{
(void)flags; CHECK(fd==0);
++recv_calls;
CHECK(socket_timeout_ms>0); /* zero would enter portMAX_DELAY in IDF */
if (incoming_length) {
CHECK(length>=incoming_length); memcpy(data,incoming,incoming_length);
memcpy(source,&incoming_source,sizeof(incoming_source)); *source_length=sizeof(incoming_source);
size_t result=incoming_length; incoming_length=0; return result;
}
errno=recv_error ? recv_error : EAGAIN; return -1;
}
int fake_close(int fd) { CHECK(fd==0); ++closed; return 0; }
unsigned int fake_if_nametoindex(const char *name) { return strcmp(name,"st1")==0 ? 2U : 0U; }
char *fake_if_indextoname(unsigned int index,char *name) { if (index!=2 && index!=3) return NULL; strcpy(name,index==2?"st1":"ap2"); return name; }
esp_netif_t *esp_netif_get_handle_from_ifkey(const char *key) { static int sta=2,ap=3; return strcmp(key,"WIFI_STA_DEF")==0?&sta:&ap; }
int esp_netif_get_netif_impl_index(esp_netif_t *p) { return *p; }
size_t strlcpy(char *dest,const char *source,size_t size) { size_t n=strlen(source); if (size) snprintf(dest,size,"%s",source); return n; }
const char *esp_err_to_name(esp_err_t error) { (void)error; return "fake error"; }
void *heap_caps_malloc(size_t size,int caps) { CHECK(caps==(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT)); return allocation_fail?NULL:malloc(size); }
QueueHandle_t xQueueCreateStatic(unsigned int count,unsigned int size,uint8_t *bytes,StaticQueue_t *control) { (void)control; CHECK(count==21); queue_bytes=bytes; queue_size=size; queue_read=queue_write=0; return bytes; }
int xQueueReset(QueueHandle_t q) { CHECK(q==queue_bytes); queue_read=queue_write=0; return pdTRUE; }
int xQueueSend(QueueHandle_t q,const void *event,unsigned int timeout) { CHECK(q==queue_bytes && timeout==0 && queue_write<21); memcpy(queue_bytes+queue_size*queue_write++,event,queue_size); return pdTRUE; }
int xQueueReceive(QueueHandle_t q,void *event,unsigned int timeout) { (void)timeout; CHECK(q==queue_bytes); if (queue_read==queue_write) return 0; memcpy(event,queue_bytes+queue_size*queue_read++,queue_size); return pdTRUE; }
const char *ipaddr_ntoa_r(const ip_addr_t *ip,char *out,int capacity) { return inet_ntop(ip->family,ip->family==AF_INET?(const void *)&ip->v4:(const void *)&ip->v6.addr,out,capacity); }
esp_err_t esp_ping_get_profile(esp_ping_handle_t h,int profile,void *out,size_t size) { CHECK(h==&ping_config); memset(out,0,size); if (profile==ESP_PING_PROF_IPADDR) memcpy(out,&ping_config.target_addr,size); if (profile==ESP_PING_PROF_REQUEST || profile==ESP_PING_PROF_REPLY) *(uint32_t *)out=ping_config.count; return ESP_OK; }
esp_err_t esp_ping_new_session(const esp_ping_config_t *config,const esp_ping_callbacks_t *callbacks,esp_ping_handle_t *out) { ++ping_created; ping_config=*config; ping_callbacks=*callbacks; if(ping_create_error) return -1; *out=&ping_config; return ESP_OK; }
esp_err_t esp_ping_start(esp_ping_handle_t h) { if(ping_start_error) return -1; if(ping_delayed) return ESP_OK; ping_callbacks.on_ping_success(h,ping_callbacks.cb_args); ping_callbacks.on_ping_end(h,ping_callbacks.cb_args); return ESP_OK; }
esp_err_t esp_ping_delete_session(esp_ping_handle_t h) { CHECK(h==&ping_config); ++ping_deleted; return ESP_OK; }
esp_err_t esp_ping_stop(esp_ping_handle_t h) { CHECK(h==&ping_config); return ESP_OK; }
int64_t esp_timer_get_time(void) { now+=1000; return now; }
uint16_t inet_chksum(const void *data,u16_t length) { uint32_t sum=checksum_sum(data,length,0); while(sum>>16) sum=(sum&65535)+(sum>>16); return htons((uint16_t)~sum); }
esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd) { CHECK(cmd->func==network_console_execute); return ESP_OK; }
static void test_arguments(void)
{
diagnostic_arguments_t a;
char *good[]={"ping","-6","::1","20"};
CHECK(parse_arguments(4,good,4,20,&a) && a.family==AF_INET6 && a.limit==20);
char *after[]={"ping","::1","20","-6"};
CHECK(parse_arguments(4,after,4,20,&a));
char *middle[]={"ping","::1","-6","1"};
CHECK(parse_arguments(4,middle,4,20,&a) && a.limit==1);
char *bad[][5]={{"ping","-6","::1","-6",NULL},{"ping","-4","::1","-6",NULL},
{"ping","host","0",NULL},{"ping","host","21",NULL},{"ping","host","-1",NULL},
{"ping","host","42949672960",NULL},{"ping","host","1x",NULL},{"ping","",NULL},
{"ping","-6",NULL},{"ping","host","1","2",NULL},{"ping","--", "host",NULL}};
for(size_t i=0;i<sizeof(bad)/sizeof(bad[0]);++i) { int n=0; while(bad[i][n])++n; CHECK(!parse_arguments(n,bad[i],4,20,&a)); }
char *lookup[]={"nslookup","host","1"}; CHECK(!parse_arguments(3,lookup,0,0,&a));
char *trace[]={"traceroute","host","30"}; CHECK(parse_arguments(3,trace,16,30,&a)); trace[2]="31"; CHECK(!parse_arguments(3,trace,16,30,&a));
}
static diagnostic_target_t target_for(const char *text)
{
diagnostic_target_t target; CHECK(parse_literal(text,AF_UNSPEC,&target)==1); return target;
}
static void test_resolution(void)
{
diagnostic_target_t target; char numeric[48];
const char *valid[]={"fe80::1%sta","fe80::1%ap","fe80::1%2","fe80::1%st1","fe80::1","::1","192.0.2.1"};
for(size_t i=0;i<sizeof(valid)/sizeof(valid[0]);++i) CHECK(parse_literal(valid[i],AF_UNSPEC,&target)==1);
const char *bad[]={"fe80::1%0","fe80::1%256","fe80::1%9","fe80::1%","fe80::1%eth0","fe80::1%sta%2","example%sta","192.0.2.1%sta","::ffff:192.0.2.1","fe80:garbage"};
for(size_t i=0;i<sizeof(bad)/sizeof(bad[0]);++i) CHECK(parse_literal(bad[i],AF_UNSPEC,&target)==-1);
CHECK(parse_literal("::1",AF_INET,&target)==-1); CHECK(parse_literal("192.0.2.1",AF_INET6,&target)==-1);
target=target_for("fe80::1%sta"); CHECK(sockaddr_to_numeric((void *)&target.address,target.length,numeric,sizeof(numeric))); CHECK(strcmp(numeric,"fe80::1%2")==0);
CHECK(!sockaddr_to_numeric((void *)&target.address,target.length,numeric,4));
ip_addr_t ip; struct addrinfo entry={.ai_family=AF_INET6,.ai_addr=(void *)&target.address,.ai_addrlen=target.length};
CHECK(addrinfo_to_ip_addr(&entry,&ip)); CHECK(ip.v6.zone==2);
diagnostic_arguments_t args={.host="example.test",.family=AF_UNSPEC};
reset(); CHECK(resolve_target(&args,&target,numeric,sizeof(numeric))==0); CHECK(query_count==1 && queries[0]==AF_INET && freed==1);
reset(); resolver_error4=EAI_FAIL; CHECK(resolve_target(&args,&target,numeric,sizeof(numeric))==0); CHECK(query_count==2 && queries[1]==AF_INET6 && freed==1);
reset(); resolver_error4=EAI_MEMORY; CHECK(resolve_target(&args,&target,numeric,sizeof(numeric))!=0); CHECK(query_count==1);
reset(); args.family=AF_INET6; resolver_error6=EAI_FAIL; CHECK(resolve_target(&args,&target,numeric,sizeof(numeric))!=0); CHECK(query_count==1 && queries[0]==AF_INET6);
char *lookup[]={"nslookup","example.test"}; reset(); CHECK(network_console_execute(2,lookup)==0); CHECK(query_count==2 && freed==2);
char *lookup6[]={"nslookup","example.test","-6"}; reset(); CHECK(network_console_execute(3,lookup6)==0); CHECK(query_count==1 && queries[0]==AF_INET6);
char *lookup4[]={"nslookup","-4","example.test"}; reset(); CHECK(network_console_execute(3,lookup4)==0); CHECK(query_count==1 && queries[0]==AF_INET);
reset(); resolver_error4=EAI_NONAME; CHECK(network_console_execute(2,lookup)==0); CHECK(query_count==2 && queries[1]==AF_INET6);
reset(); resolver_error4=resolver_error6=EAI_FAIL; CHECK(network_console_execute(2,lookup)!=0); CHECK(query_count==2);
}
static int capture_command(int argc, char **argv, char *output, size_t capacity)
{
fflush(stdout);
int saved=dup(STDOUT_FILENO);
FILE *capture=tmpfile();
CHECK(saved>=0 && capture!=NULL);
CHECK(dup2(fileno(capture),STDOUT_FILENO)>=0);
int result=network_console_execute(argc,argv);
fflush(stdout);
CHECK(fseek(capture,0,SEEK_SET)==0);
size_t size=fread(output,1,capacity-1,capture);
CHECK(size<capacity-1 && !ferror(capture));
output[size]='\0';
CHECK(dup2(saved,STDOUT_FILENO)>=0);
CHECK(close(saved)==0);
CHECK(fclose(capture)==0);
return result;
}
static void test_unscoped_linklocal(void)
{
char output[1024];
diagnostic_target_t target;
reset(); resolver_linklocal=true;
CHECK(resolve_family("ll.example",AF_INET6,&target)==0);
CHECK(target_needs_scope(&target) && freed==1);
const char *hosts[]={"ll.example","fe80::1"};
for(size_t h=0;h<2;++h) {
for(int explicit6=0;explicit6<=1;++explicit6) {
char *lookup[]={"nslookup",(char *)hosts[h],"-6"};
reset(); resolver_linklocal=true;
CHECK(capture_command(explicit6?3:2,lookup,output,sizeof(output))==0);
CHECK(strstr(output,h==0?"AAAA: fe80::1":"Address: fe80::1 (numeric literal; no DNS query)")!=NULL);
CHECK(strstr(output,"explicit device scope")!=NULL && strstr(output,"fe80::1%sta")!=NULL);
CHECK(query_count==(h==0?(explicit6?1:2):0));
if(h==0) CHECK(queries[query_count-1]==AF_INET6);
CHECK(sockets==0 && ping_created==0);
const char *commands[]={"ping","traceroute"};
for(size_t c=0;c<2;++c) {
char *probe[]={(char *)commands[c],(char *)hosts[h],"1","-6"};
reset(); resolver_linklocal=true; resolver_error4=EAI_NONAME;
CHECK(capture_command(explicit6?4:3,probe,output,sizeof(output))!=0);
CHECK(strstr(output,"Link-local fe80::1")!=NULL && strstr(output,"fe80::1%sta")!=NULL);
CHECK(strstr(output,"device interface, not the SSH/browser client's")!=NULL);
CHECK(sockets==0 && sent_count==0 && ping_created==0);
CHECK(query_count==(h==0?(explicit6?1:2):0));
}
}
}
}
static void put16(uint8_t *p,uint16_t n) { p[0]=n>>8; p[1]=n; }
static void fix_packet(uint8_t *p,size_t n,bool v6)
{
size_t h=v6?40:20;
p[h+2]=p[h+3]=0;
if(v6) {
uint32_t sum=checksum_sum(p+8,32,checksum_sum(p+h,n-h,(uint32_t)(n-h)+58));
while(sum>>16) sum=(sum&65535)+(sum>>16);
put16(p+h+2,(uint16_t)~sum);
} else {
p[10]=p[11]=0; uint16_t c=inet_chksum(p,20); memcpy(p+10,&c,2);
c=inet_chksum(p+h,n-h); memcpy(p+h+2,&c,2);
}
}
static size_t packet_for(uint8_t *p,bool v6,bool error)
{
size_t h=v6?40:20, n=h+8+(error?h+8:0);
memset(p,0,1280); p[0]=v6?0x60:0x45;
if(v6) { put16(p+4,n-h); p[6]=58; inet_pton(AF_INET6,"2001:db8::1",p+8); inet_pton(AF_INET6,"2001:db8::2",p+24); }
else { put16(p+2,n); p[9]=1; inet_pton(AF_INET,"192.0.2.1",p+12); inet_pton(AF_INET,"192.0.2.2",p+16); }
uint8_t *echo=p+h;
echo[0]=error?(v6?3:11):(v6?129:0);
if(error) {
uint8_t *inner=echo+8; inner[0]=v6?0x60:0x45;
if(v6) { put16(inner+4,8); inner[6]=58; inet_pton(AF_INET6,"2001:db8::1",inner+24); }
else { put16(inner+2,28); inner[9]=1; inet_pton(AF_INET,"192.0.2.1",inner+16); uint16_t c=inet_chksum(inner,20); memcpy(inner+10,&c,2); }
echo=inner+h; echo[0]=v6?128:8;
}
put16(echo+4,0x1234); put16(echo+6,7); fix_packet(p,n,v6); return n;
}
static void test_packets(void)
{
uint8_t packet[1280],code; uint16_t id=htons(0x1234),seq=htons(7);
for(int v6=0;v6<=1;++v6) {
diagnostic_target_t target=target_for(v6?"2001:db8::1":"192.0.2.1");
for(int error=0;error<=1;++error) {
size_t n=packet_for(packet,v6,error),h=v6?40:20;
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==(error?TRACE_REPLY_HOP:TRACE_REPLY_DESTINATION));
for(size_t truncated=0;truncated<n;++truncated) CHECK(parse_trace_reply(packet,truncated,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
CHECK(parse_trace_reply(packet,n,&target,id+1,seq,&code)==TRACE_REPLY_UNRELATED);
CHECK(parse_trace_reply(packet,n,&target,id,seq+1,&code)==TRACE_REPLY_UNRELATED);
packet[n-1]^=1; CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED); packet[n-1]^=1;
packet[h+1]=255; fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,v6,error);
if(error) {
packet[n-7]=1; fix_packet(packet,n,v6); /* quoted echo code */
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,v6,error);
packet[n-8]=0xff; fix_packet(packet,n,v6); /* quoted echo type */
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,v6,error);
put16(packet+h+8+(v6?4:2),v6?7:27); fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,v6,error);
packet[h]=v6?1:3; packet[h+1]=1; fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNREACHABLE && code==1);
packet[h]=v6?3:11; packet[h+1]=1; fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,v6,error); packet[h+8+(v6?24:16)]^=1; fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,v6,error); packet[h+8+(v6?6:9)]=v6?44:17; fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
} else {
packet[v6?8:12]^=1; fix_packet(packet,n,v6);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
}
}
}
diagnostic_target_t target=target_for("2001:db8::1");
for(unsigned i=0;i<6;++i) {
const uint8_t unsupported[]={0,43,44,50,51,60};
size_t n=packet_for(packet,true,true); packet[6]=unsupported[i]; fix_packet(packet,n,true);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
packet_for(packet,true,true); packet[48+6]=unsupported[i]; fix_packet(packet,n,true);
CHECK(parse_trace_reply(packet,n,&target,id,seq,&code)==TRACE_REPLY_UNRELATED);
}
/* Packet fuzz lengths and bytes exercise actual parser bounds deterministically. */
uint32_t random=7;
for(unsigned i=0;i<10000;++i) { random=random*1664525U+1013904223U; size_t n=random%sizeof(packet); for(size_t j=0;j<n;++j) { random=random*1664525U+1013904223U; packet[j]=random>>24; } (void)parse_trace_reply(packet,n,&target,id,seq,&code); }
}
static void test_submillisecond_deadline(void)
{
const int remaining[]={1,500,999,1000};
for(int v6=0;v6<=1;++v6) {
diagnostic_target_t target=target_for(v6?"2001:db8::1":"192.0.2.1");
char source[48]; int64_t rtt; uint8_t code;
for(size_t i=0;i<sizeof(remaining)/sizeof(remaining[0]);++i) {
reset(); now=TRACEROUTE_TIMEOUT_US-remaining[i]-1000;
CHECK(wait_for_trace_reply(0,&target,0,0,0,source,sizeof(source),&rtt,&code)==TRACE_WAIT_TIMEOUT);
CHECK(recv_calls==(remaining[i]>=1000?1U:0U));
CHECK(options==(remaining[i]>=1000?1:0));
if(remaining[i]>=1000) CHECK(socket_timeout_ms==1);
}
/* One unrelated packet leaves exactly 500 us; no second recv allowed. */
reset(); now=TRACEROUTE_TIMEOUT_US-1500-1000;
incoming_length=packet_for(incoming,v6,false); incoming_source=target.address;
CHECK(wait_for_trace_reply(0,&target,0,0,0,source,sizeof(source),&rtt,&code)==TRACE_WAIT_TIMEOUT);
CHECK(recv_calls==1 && options==1 && socket_timeout_ms==1);
}
}
static void test_lifetimes(void)
{
for(int v6=0;v6<=1;++v6) {
char *argv[]={"traceroute",v6?"2001:db8::1":"192.0.2.1","1"};
reset(); CHECK(network_console_execute(3,argv)==0); CHECK(sockets==1 && closed==1 && sent_count==1 && ttl_options==1); CHECK(v6only_options==v6);
int option_count=options;
for(int fail=1;fail<=option_count;++fail) { reset(); fail_option=fail; CHECK(network_console_execute(3,argv)!=0); CHECK(closed==1); }
reset(); send_fail=1; CHECK(network_console_execute(3,argv)!=0); CHECK(closed==1);
reset(); recv_error=EIO; CHECK(network_console_execute(3,argv)!=0); CHECK(closed==1);
reset(); socket_fail=1; CHECK(network_console_execute(3,argv)!=0); CHECK(closed==0);
}
char *scoped[]={"traceroute","fe80::1%sta","-6","1"}; reset(); CHECK(network_console_execute(4,scoped)==0); CHECK(bind_options==1 && closed==1);
reset(); fail_option=2; CHECK(network_console_execute(4,scoped)!=0); CHECK(closed==1);
char *hostname[]={"traceroute","example.test","1"}; reset(); send_fail=1; CHECK(network_console_execute(3,hostname)!=0); CHECK(query_count==1 && queries[0]==AF_INET && closed==1);
diagnostic_target_t target=target_for("2001:db8::1"); char source[48]; int64_t rtt; uint8_t code;
reset(); now=2000000; CHECK(wait_for_trace_reply(0,&target,htons(0x1234),htons(7),0,source,sizeof(source),&rtt,&code)==TRACE_WAIT_TIMEOUT); CHECK(options==0);
reset(); incoming_length=packet_for(incoming,true,false); incoming_source=target.address;
CHECK(wait_for_trace_reply(0,&target,htons(0x1234),htons(7),0,source,sizeof(source),&rtt,&code)==TRACE_WAIT_DESTINATION);
reset(); incoming_length=packet_for(incoming,true,false); incoming_source=target_for("2001:db8::9").address;
CHECK(wait_for_trace_reply(0,&target,htons(0x1234),htons(7),0,source,sizeof(source),&rtt,&code)==TRACE_WAIT_TIMEOUT);
char *ping[]={"ping","fe80::1%sta","-6","1"};
reset(); allocation_fail=true; CHECK(network_console_execute(4,ping)!=0); CHECK(s_ping_queue_bytes==NULL);
reset(); CHECK(network_console_execute(4,ping)==0); CHECK(ping_config.interface==2 && ping_config.target_addr.v6.zone==2 && ping_deleted==1);
reset(); ping_create_error=1; CHECK(network_console_execute(4,ping)!=0); CHECK(ping_deleted==0);
reset(); ping_start_error=1; CHECK(network_console_execute(4,ping)!=0); CHECK(ping_deleted==1);
reset(); ping_delayed=true; CHECK(network_console_execute(4,ping)!=0); CHECK(s_ping_pending && ping_deleted==0);
CHECK(ping_callbacks.cb_args==&s_ping_context);
CHECK(network_console_execute(4,ping)!=0); CHECK(s_ping_pending);
ping_callbacks.on_ping_end(&ping_config,ping_callbacks.cb_args);
ping_delayed=false; CHECK(network_console_execute(4,ping)==0); CHECK(!s_ping_pending && ping_deleted==2);
reset(); recv_error=EINTR;
CHECK(wait_for_trace_reply(0,&target,0,0,0,source,sizeof(source),&rtt,&code)==TRACE_WAIT_TIMEOUT);
}
int main(void)
{
test_arguments(); test_resolution(); test_unscoped_linklocal();
test_packets(); test_submillisecond_deadline(); test_lifetimes();
CHECK(network_console_register_root_commands()==ESP_OK);
free(s_ping_queue_bytes);
printf("network diagnostics: %u checks + 10000 packet fuzz inputs PASS\n",checks);
return 0;
}
@@ -0,0 +1,53 @@
static void expect_matches(const char *prefix, const char *expected)
{
char output[CONSOLE_COMPLETION_OUTPUT_CAPACITY]={0}; size_t length=999;
assert(console_completion_format_matches(prefix,output,sizeof(output),&length));
assert(length==strlen(expected) && !memcmp(output,expected,length));
}
int main(void)
{
const char *verbs[]={"ping","nslookup","traceroute"};
for (unsigned alias=0;alias<2;++alias)
for (unsigned verb=0;verb<3;++verb) {
char base[64], prefix[128], expected[256], expanded[128];
snprintf(base,sizeof(base),"%s%s",alias ? "wifi " : "",verbs[verb]);
snprintf(prefix,sizeof(prefix),"%s ",base);
snprintf(expected,sizeof(expected),"%s -4\r\n%s -6\r\n",base,base);
expect_matches(prefix,expected);
assert(console_completion_expand(prefix,expanded,sizeof(expanded)));
snprintf(prefix,sizeof(prefix),"%s -",base);
assert(!strcmp(expanded,prefix));
expect_matches(prefix,expected);
assert(!console_completion_expand(prefix,expanded,sizeof(expanded)));
snprintf(prefix,sizeof(prefix),"%s -6",base);
expect_matches(prefix,"");
assert(!console_completion_expand(prefix,expanded,sizeof(expanded)));
snprintf(prefix,sizeof(prefix),"%s -4 ",base); expect_matches(prefix,"");
snprintf(prefix,sizeof(prefix),"%s -6 ",base); expect_matches(prefix,"");
snprintf(prefix,sizeof(prefix),"%s private-host.example -",base); expect_matches(prefix,"");
snprintf(prefix,sizeof(prefix),"%s 2001:db8::1 ",base); expect_matches(prefix,"");
snprintf(prefix,sizeof(prefix),"%s -",base);
size_t length=123;
assert(!console_completion_format_matches(prefix,expanded,1,&length));
assert(length==123);
snprintf(prefix,sizeof(prefix),"%s ",base);
assert(!console_completion_expand(prefix,expanded,strlen(prefix)+1));
}
expect_matches("wifi profile secret 0 ","");
expect_matches("wifi ap secret ","");
expect_matches("user password admin ","");
expect_matches("nslook","nslookup\r\n");
expect_matches("wifi p","wifi profiles\r\nwifi profile\r\nwifi profile set\r\nwifi profile secret\r\nwifi profile enable\r\nwifi profile disable\r\nwifi profile delete\r\nwifi ping\r\nwifi ping -4\r\nwifi ping -6\r\n");
/* Every candidate prefix must still fit the shared bounded list buffer. */
for (size_t i=0;i<sizeof(s_completion_candidates)/sizeof(s_completion_candidates[0]);++i) {
char prefix[257], output[CONSOLE_COMPLETION_OUTPUT_CAPACITY]; size_t length;
strcpy(prefix,s_completion_candidates[i]);
for (size_t n=0;n<=strlen(s_completion_candidates[i]);++n) {
char saved=prefix[n]; prefix[n]='\0';
assert(console_completion_format_matches(prefix,output,sizeof(output),&length));
prefix[n]=saved;
}
}
puts("PASS: actual shared completion matcher, six diagnostic prefixes, no host/secret guessing, expansion and bounded output");
}
@@ -0,0 +1,80 @@
static bool owner_live=true;
static bool is_current(const admin_ssh_console_token_t *token, const user_principal_t *principal)
{ (void)token; assert(principal->role==USER_ROLE_ADMIN); return owner_live; }
static bool drained(const admin_ssh_console_token_t *token) { (void)token; return true; }
static esp_err_t perform(const admin_ssh_console_token_t *token,
admin_ssh_deferred_action_type_t action, uint32_t argument)
{ (void)token; (void)action; (void)argument; assert(false); return ESP_FAIL; }
static const admin_console_owner_t owner={.is_current=is_current,.drained=drained,.perform=perform};
static void pump(void) { if (!setjmp(loop_done)) worker_task(NULL); }
static void feed(const admin_ssh_console_token_t *token, const char *line)
{
char input[257]; snprintf(input,sizeof(input),"%s\r",line);
size_t used=0;
assert(admin_ssh_console_feed_input(token,(const uint8_t *)input,strlen(input),&used));
assert(used==strlen(input));
}
static void discard_output(const admin_ssh_console_token_t *token)
{
uint8_t output[4096]; size_t used;
assert(admin_ssh_console_read_output(token,output,sizeof(output),&used)==ESP_OK);
}
int main(void)
{
assert(admin_ssh_console_init()==ESP_OK);
assert(admin_ssh_console_start_uart_frontend()==ESP_OK);
const user_principal_t admin={.role=USER_ROLE_ADMIN};
const user_principal_t ordinary={.role=USER_ROLE_USER};
const char *verbs[]={"ping","nslookup","traceroute"};
const char *hosts[]={"example.org","192.0.2.1","2001:db8::1","fe80::1%st1"};
unsigned cases=0;
for (unsigned transport=0;transport<2;++transport) {
admin_ssh_console_token_t token={.slot_index=0,.session_id=7,.slot_generation=1,
.transport=transport ? ADMIN_CONSOLE_TRANSPORT_WEB : ADMIN_CONSOLE_TRANSPORT_SSH};
assert(admin_ssh_console_open_owned(&token,&ordinary,&owner)!=ESP_OK);
assert(!s_sessions[0].active && !s_request_queue->count);
assert(admin_ssh_console_open_owned(&token,&admin,&owner)==ESP_OK);
for (unsigned alias=0;alias<2;++alias)
for (unsigned verb=0;verb<3;++verb)
for (unsigned host=0;host<4;++host)
for (unsigned family=0;family<3;++family)
for (unsigned position=0;position<3;++position) {
const char *flag=family==0 ? "" : family==1 ? "-4 " : "-6 ";
const char *count=verb==0 ? "20 " : verb==2 ? "30 " : "";
char line[257];
snprintf(line,sizeof(line),"%s%s %s%s %s%s%s",alias ? "wifi " : "",verbs[verb],
position==0 ? flag : "",hosts[host],position==1 ? flag : "",count,
position==2 ? flag : "");
unsigned before=runs;
discard_output(&token);
feed(&token,line); pump();
assert(runs==before+1 && !strcmp(dispatched,line));
/* Trusted UART0 arrives at exactly the same registry boundary. */
admin_request_t uart={.origin=ADMIN_REQUEST_UART0};
memcpy(uart.line,line,strlen(line)+1);
assert(xQueueSend(s_request_queue,&uart,0)); pump();
assert(runs==before+2 && !strcmp(dispatched,line));
++cases;
}
const char *extra[]={"ping example.org", "wifi ping example.org", "traceroute example.org",
"wifi traceroute example.org", "nslookup example.org", "wifi nslookup example.org",
" \"wifi\" \"ping\" \"2001:db8::1\" \"2\" \"-6\" ",
"\"nslookup\" \"-6\" \"example.org\""};
for (unsigned i=0;i<sizeof(extra)/sizeof(extra[0]);++i) {
discard_output(&token); unsigned before=runs;
feed(&token,extra[i]); pump();
assert(runs==before+1 && !strcmp(dispatched,extra[i]));
}
/* Existing currentness guards deny diagnostics after revocation. */
unsigned before=runs;
feed(&token,"wifi ping -6 2001:db8::1"); principal_current=false; pump();
assert(runs==before && !s_sessions[0].active);
principal_current=true; ++token.slot_generation;
assert(admin_ssh_console_open_owned(&token,&admin,&owner)==ESP_OK);
feed(&token,"nslookup -6 example.org"); owner_live=false; pump();
assert(runs==before && !s_sessions[0].active);
owner_live=true;
}
printf("PASS: %u SSH/WEB root+Wi-Fi family/position/host cases and UART0 parity; defaults/quotes, ordinary-role and revocation guards\n",cases);
}
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Host surface regressions; real dispatcher/matcher, fake RTOS and console sink.
No sockets, diagnostic core execution, target scheduler or hardware are exercised.
"""
import os
from pathlib import Path
import re
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[2]
IDF = Path(os.environ.get("IDF_PATH", str(Path.home() / ".platformio/packages/framework-espidf")))
def read(path):
return (ROOT / path).read_text()
def stripped(text):
return "\n".join(line for line in text.splitlines()
if not line.startswith(("#include", "#pragma once")))
# Guard canonical registry and unchanged argv forwarding, including UART0 wiring.
network = read("src/network_console.c")
wifi = read("src/wifi_console.c")
main = read("src/main.c")
admin = read("src/admin_ssh_console.c")
registry = network[network.index("esp_err_t network_console_register_root_commands("):]
for name in ("ping", "nslookup", "traceroute"):
assert re.search(r'\.command\s*=\s*"' + name + r'"[^}]*\.func\s*=\s*&network_console_execute', registry)
assert f'strcmp(name, "{name}") == 0' in network
assert re.search(r'if \(argc >= 2 && network_console_is_command\(argv\[1\]\)\)\s*\{\s*'
r'/\*.*?\*/\s*return network_console_execute\(argc - 1, argv \+ 1\);\s*\}', wifi, re.S)
assert re.search(r'\.command\s*=\s*"wifi"[^}]*\.func\s*=\s*&command_wifi', wifi)
for registration in ("wifi_console_register_commands", "network_console_register_root_commands"):
assert f"ESP_ERROR_CHECK({registration}());" in main
uart = admin[admin.index("static void uart_frontend_task("):]
assert ".origin = ADMIN_REQUEST_UART0" in uart
assert "request.line" in uart and "xQueueSend(s_request_queue, &request" in uart
assert "dispatch_registered_command(&request);" in admin
assert "esp_console_run((const char *)request->line, &command_result)" in admin
print("PASS: root/Wi-Fi registry, exact shared argv forwarding and UART0 queue/dispatcher source contracts")
with tempfile.TemporaryDirectory(prefix="network-surfaces-") as directory:
path = Path(directory)
# Observe the actual dispatcher output at the ESP-IDF registry boundary.
fakes = read("tests/admin_console_boundary/fakes.h")
old = "{ (void)s; ++runs; if (command_hook) command_hook(); *r=0; return ESP_OK; }"
assert old in fakes
fakes = fakes.replace("static esp_err_t esp_console_run(",
"static char dispatched[257];\nstatic esp_err_t esp_console_run(")
fakes = fakes.replace(old, "{ assert(strlen(s)<sizeof(dispatched)); strcpy(dispatched,s); ++runs; if (command_hook) command_hook(); *r=0; return ESP_OK; }")
unit = (fakes + stripped(read("src/admin_ssh_console.h")) + "\n" + stripped(admin)
+ read("tests/network_diagnostics_surfaces/routing.c"))
(path / "routing.c").write_text(unit)
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-variable",
str(path / "routing.c"), str(IDF / "components/console/split_argv.c"),
"-o", str(path / "routing")], check=True, timeout=30)
subprocess.run([str(path / "routing")], check=True, timeout=10)
completion = read("src/console_completion.c")
# All shared matching functions, excluding only UART read/linenoise adapters.
completion = completion[:completion.index("static ssize_t console_read_with_late_terminal_upgrade(")]
unit = ("#include <assert.h>\n#include <stdbool.h>\n#include <stddef.h>\n#include <stdio.h>\n#include <string.h>\n"
+ stripped(read("src/console_completion.h")) + "\n" + stripped(completion)
+ read("tests/network_diagnostics_surfaces/completion.c"))
(path / "completion.c").write_text(unit)
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
str(path / "completion.c"), "-o", str(path / "completion")], check=True, timeout=30)
subprocess.run([str(path / "completion")], check=True, timeout=10)