# Shared IPv4/IPv6 diagnostics fixtures and integration handoff ## Commands ```text ping [-4|-6] [count] count 1..20, default 4 traceroute [-4|-6] [max-hops] hops 1..30, default 16 nslookup [-4|-6] ``` 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.