Shared IPv4/IPv6 diagnostics fixtures and integration handoff
Commands
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
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_INETandAF_INET6choose explicit DNS address types. Current generatedCONFIG_LWIP_DNS_MAX_HOST_IP=1means one returned address per query; this command prints one selected address per family, not a complete RRset. DNS errors collapse intoEAI_FAIL; NXDOMAIN, timeout, and server failure cannot reliably be distinguished. Resolution itself uses SDK DNS timeouts, not a command-owned deadline..localdepends on lwIP mDNS-query support, not client NSS.core/ipv6/ip6.crestores the complete IPv6 header beforeraw_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.cdemultiplexes 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.cenables checksum generation at offset 2 for raw ICMPv6 sockets.api/sockets.crefuses changingIPV6_CHECKSUMfor ICMPv6; do not set it.IPPROTO_IP/IP_TTLsets the common PCB TTL used for both IPv4 TTL and IPv6 hop limit.IPV6_UNICAST_HOPSis not implemented. Fixtures require the actual supported option for both families. IPv6 traceroute also setsIPV6_V6ONLY. SDK ping'sIPPROTO_IP/IP_TTLuse is likewise correct for IPv6; it is unchanged.api/sockets.cfloorsSO_RCVTIMEOtimeval values to milliseconds;port/freertos/sys_arch.c:sys_arch_mbox_fetch()interprets zero milliseconds asportMAX_DELAY, not a poll. Traceroute stops when <1000 us remain, never supplying a zero-millisecond receive timeout.apps/ping/ping_sock.cdoes not copy the target zone intosin6_scope_id;config.interfaceusesSO_BINDTODEVICEand 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.ccompilation arguments. Nopiobuild, 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-usageframes:execute_ping400 -> 464;execute_nslookup128 -> 144;execute_traceroute224 -> 256;wait_for_trace_reply240 -> 1408. These individual frames are not a measured runtime stack high-water mark. - ASan/UBSan attempt could not link: host
libasan.so.8.0.0andlibubsan.so.1.0.0are missing. Ordinary-Wall -Wextra -Werrorfixtures 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.