Add Dual-Stack Network Diagnostics
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user