Add Dual-Stack Network Diagnostics
This commit is contained in:
@@ -187,6 +187,14 @@ Admin SSH `exit`, remote reboot, SSH stop/disconnect, and host-key rotate/reset
|
|||||||
|
|
||||||
Browser stop/reboot uses this same owner-adapter control path. Exact forced certificate rotation instead uses the typed queue union and immutable `dispatcher_actions` mask to hand off after drain/200 ms to the existing 12 KiB dispatcher, not the 4 KiB control stack. Pending input is discarded through execution and an executing slot remains reserved across self-detach. Canonical shared service/identity replacement preserves commit/stop/start failure semantics. Parsed other-account interactive add/password and forced role/delete are allowed. Canonical Wi-Fi commands are also allowed, including hidden secret prompts and disruptive changes; these use the ordinary owner queue and may cut off the browser before output arrives, without a deferred acknowledgement guarantee. Browser self/generated/key/recovery, mDNS mutations and restricted SSH shell actions remain denied; typed Settings has separate permissions. Post-prompt currentness is operation admission, not an atomic session-liveness/NVS guarantee. [Browser policy and recovery](../web_administration.md#browser-shell-policy).
|
Browser stop/reboot uses this same owner-adapter control path. Exact forced certificate rotation instead uses the typed queue union and immutable `dispatcher_actions` mask to hand off after drain/200 ms to the existing 12 KiB dispatcher, not the 4 KiB control stack. Pending input is discarded through execution and an executing slot remains reserved across self-detach. Canonical shared service/identity replacement preserves commit/stop/start failure semantics. Parsed other-account interactive add/password and forced role/delete are allowed. Canonical Wi-Fi commands are also allowed, including hidden secret prompts and disruptive changes; these use the ordinary owner queue and may cut off the browser before output arrives, without a deferred acknowledgement guarantee. Browser self/generated/key/recovery, mDNS mutations and restricted SSH shell actions remain denied; typed Settings has separate permissions. Post-prompt currentness is operation admission, not an atomic session-liveness/NVS guarantee. [Browser policy and recovery](../web_administration.md#browser-shell-policy).
|
||||||
|
|
||||||
|
### Shared IPv4/IPv6 diagnostics
|
||||||
|
|
||||||
|
`network_console` owns root and `wifi`-alias ping/traceroute/nslookup for UART0, admin SSH and browser Admin. Family flags are parsed once; explicit IPv6 never silently falls back to IPv4. Hostname probes use IPv4-first resolution fallback only; nslookup queries A and AAAA separately and shows unscoped link-local answers. Probes require a device-local link-local zone (`%sta`, `%ap`, valid index/name). Resolver policy/DNS provisioning and serial transport ownership are unchanged.
|
||||||
|
|
||||||
|
Traceroute owns one raw socket and a 1280-byte transient receive buffer on the shared dispatcher stack, one eight-byte probe per hop (max30), and one-second absolute receive deadlines. It rejects incomplete/corrupt/unrelated packets and unsupported IPv6 extension headers/fragments. IDF5.5.0-specific framing, automatic ICMPv6 checksum, common `IP_TTL` hop-limit behavior and millisecond timeout flooring are pinned/retested; <1ms remaining returns timeout rather than configuring an infinite wait. DNS and send/scheduler latency are not covered by the receive deadline.
|
||||||
|
|
||||||
|
Ping keeps the 4200-byte PSRAM-only lazy queue payload and SDK transient task. Permanent callback context plus a pending reservation survives the count-based console deadline; later pings cannot reset a live producer's queue. SDK receive can outlast its timeout under unrelated ICMP, so a timed-out session stays reserved until END. END means callback production ended, not synchronous task/socket destruction; retiring resources can briefly overlap a new session. These diagnostics serialize administration but never execute on HTTPD or enter the serial hot path. Tests and exact limits: `tests/network_diagnostics/README.md`.
|
||||||
|
|
||||||
## Wi-Fi and persistence
|
## Wi-Fi and persistence
|
||||||
|
|
||||||
`wifi_config` owns a fixed-width versioned NVS schema with four prioritized station profiles and AP policy `off`, `fallback`, or `always`. Missing configuration generates per-device defaults including a random AP password. Invalid stored data is generally left untouched while RAM defaults are used.
|
`wifi_config` owns a fixed-width versioned NVS schema with four prioritized station profiles and AP policy `off`, `fallback`, or `always`. Missing configuration generates per-device defaults including a random AP password. Invalid stored data is generally left untouched while RAM defaults are used.
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ Shared UI regression: `tests/web_ui_session/run.py` and its domain `.cjs` fixtur
|
|||||||
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
|
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
|
||||||
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Admin SSH `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort output draining.
|
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Admin SSH `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort output draining.
|
||||||
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
|
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
|
||||||
|
- Dual-stack diagnostics: `network_console.*` is the shared root/`wifi` handler on all three admin surfaces. `-4`/`-6`, device-local `%sta`/`%ap`/index/name; default probes IPv4-first resolution fallback only, nslookup explicit A+AAAA and unscoped link-local display. One-second/hop raw IPv4/IPv6 traceroute validates full outer packet/quoted echo and rejects IPv6 extensions/fragments. IDF5.5.0 raw framing/hoplimit/checksum/timeout contract is version-guarded. Ping uses existing PSRAM queue and SDK transient task with permanent callback context/deadline/pending reservation; END does not imply completed SDK teardown. Tests: `tests/network_diagnostics` (actual handlers/packet and SDK contracts), `tests/network_diagnostics_surfaces` (routing/policy/completion). No serial hot-path changes.
|
||||||
|
|
||||||
## Wi-Fi
|
## Wi-Fi
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), [architecture](architecture.md) and [decisions](design-decisions.md).
|
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), [architecture](architecture.md) and [decisions](design-decisions.md).
|
||||||
|
|
||||||
|
## Dual-stack diagnostics follow-up — implemented, device validation pending
|
||||||
|
|
||||||
|
- User explicitly requested IPv6 ping/traceroute/nslookup on all surfaces after accepting Phase12. Shared `network_console.*` now supports one `-4`/`-6` anywhere after command, preserves count/hop bounds, root and `wifi` aliases. Default probes try IPv4 then IPv6 on resolution failure only; nslookup performs A+AAAA queries separately (one selected address/family), displays unscoped link-local records. Link-local probes require ESP-local `%sta`/`%ap`/index/name; no client-interface guess or IPv4-mapped fallback. Help/completion updated; permissions unchanged.
|
||||||
|
- Actual IDF5.5.0 audit: raw ICMPv6 includes outer IPv6 header, automatic TX checksum, common `IPPROTO_IP/IP_TTL` hoplimit; replies validate checksums/target/id/seq and reject extensions/fragments. Traceroute one socket/max30 probes/1s receive deadline, 1280B receive buffer. Review caught sub-ms timeout flooring to infinite SDK wait; <1000us now returns timeout, actual SDK conversion/mailbox tests verify. No dependency/config/partition/serial path change.
|
||||||
|
- Ping retains4200B PSRAM queue, max20 probes and SDK transient task; permanent callback context plus pending reservation protects delayed END after count*2+2 second console deadline. SDK unrelated-ICMP receive loop may outlive timeout; later pings fail busy until END, which does not prove synchronous resource destruction. Read exact limits in command reference/tests README. IPv6-only unicast DNS still requires a configured resolver; no RDNSS/DHCPv6 provisioning added.
|
||||||
|
- `pio run` PASS **94,452 B linked RAM / 1,857,673 B flash**, +8/+3,188 versus accepted prior build. Traceroute receive-wait object frame measured240→1408B (not runtime stack high-water). No new permanent task/socket/queue capacity. Hardware/ changes in working tree belong to other agent and were not read/edited.
|
||||||
|
- Parent PASS: production diagnostics8027 checks+10k malformed packet inputs, seven installedSDK hashes+actualtimeout fixtures, 432 SSH/WEB root+alias routing cases/UART parity/ordinary-role and revocation guards/completion, existing admin policy and browser Wi-Fi secret fixture. Independent review fixes verified no remaining actionable findings in scoped paths. ASan/UBSan host runtimes absent. Broader console-boundary pre-existing missing rebootfake remains (not changed). No upload/device/network test; Phase12 acceptance remains valid only for its recorded baseline, not this follow-up.
|
||||||
|
|
||||||
## Phase 12 COMPLETE — explicit user validation
|
## Phase 12 COMPLETE — explicit user validation
|
||||||
|
|
||||||
- User states: “I just validated Phase 12 successfully.” Record acceptance, not a pending phase. Canonical device evidence and reusable regression guidance: [Phase 12](../roadmap.md#phase-12--advanced-network-integration). No unreported DHCP capture, DNS-zone update, exhaustive address/service transition, fault/soak or byte-integrity pass is implied.
|
- User states: “I just validated Phase 12 successfully.” Record acceptance, not a pending phase. Canonical device evidence and reusable regression guidance: [Phase 12](../roadmap.md#phase-12--advanced-network-integration). No unreported DHCP capture, DNS-zone update, exhaustive address/service transition, fault/soak or byte-integrity pass is implied.
|
||||||
|
|||||||
@@ -108,12 +108,38 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|
|||||||
| `wifi ap secret` / `wifi ap show-secret` | Set or reveal the AP password. |
|
| `wifi ap secret` / `wifi ap show-secret` | Set or reveal the AP password. |
|
||||||
| `wifi save|load|defaults|reset` | Persist, restore, reset in RAM, or reset and persist configuration. |
|
| `wifi save|load|defaults|reset` | Persist, restore, reset in RAM, or reset and persist configuration. |
|
||||||
| `wifi counters|clear-counters` | Show or clear Wi-Fi counters. |
|
| `wifi counters|clear-counters` | Show or clear Wi-Fi counters. |
|
||||||
| `wifi ping <host> [count]` | Send 1–20 IPv4 or IPv6 ICMP probes. |
|
| `wifi ping [-4\|-6] <host> [count]` | IPv4/IPv6 ICMP echo; 1–20 probes, default 4. |
|
||||||
| `wifi nslookup <host>` | Resolve and display unique IPv4/IPv6 addresses. |
|
| `wifi nslookup [-4\|-6] <host>` | Explicit A/AAAA lookup; default queries both families. |
|
||||||
| `wifi traceroute <host> [max-hops]` | Run IPv4 ICMP traceroute with up to 30 hops. |
|
| `wifi traceroute [-4\|-6] <host> [max-hops]` | IPv4/IPv6 ICMP echo traceroute; 1–30 hops, default 16. |
|
||||||
|
|
||||||
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Edits to a disabled profile's SSID, priority, security mode, or secret are staged in RAM and do not interrupt the current Wi-Fi connection. Enabling or disabling a profile, changing an enabled profile, or changing AP policy/configuration applies the new radio policy and may reconnect Wi-Fi. Use `wifi save` to persist working changes. Passwords are not displayed by ordinary status output.
|
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Edits to a disabled profile's SSID, priority, security mode, or secret are staged in RAM and do not interrupt the current Wi-Fi connection. Enabling or disabling a profile, changing an enabled profile, or changing AP policy/configuration applies the new radio policy and may reconnect Wi-Fi. Use `wifi save` to persist working changes. Passwords are not displayed by ordinary status output.
|
||||||
|
|
||||||
|
### Dual-stack network diagnostics
|
||||||
|
|
||||||
|
Root `ping`, `traceroute` and `nslookup`, and their `wifi` aliases, share one implementation on **UART0, admin SSH and the browser Admin console**. They execute on the ESP32, not the client computer or the attached UART1 equipment. Ordinary serial-role users do not gain administration access. `-4` and `-6` completion is available for all six command prefixes.
|
||||||
|
|
||||||
|
```text
|
||||||
|
ping -6 example.com 4
|
||||||
|
ping example.com 4 -4
|
||||||
|
wifi ping -6 fe80::1%sta 3
|
||||||
|
traceroute -6 example.com 16
|
||||||
|
wifi traceroute -4 example.com 16
|
||||||
|
nslookup example.com
|
||||||
|
nslookup -6 peer.local
|
||||||
|
```
|
||||||
|
|
||||||
|
One optional family selector can appear before or after the host/count; duplicate/conflicting selectors and unknown options are rejected. Probe counts/hop limits are positional, not Linux `-c`/`-m` options. Without a selector, numeric addresses retain their family and probe hostnames try IPv4 resolution, then IPv6 if no address was obtained. A failed probe does **not** retry the other family; this is not Happy Eyeballs. Explicit `-6` never silently uses IPv4.
|
||||||
|
|
||||||
|
Link-local **probe destinations** require a scope on the ESP32: `%sta`, `%ap`, an existing numeric lwIP interface index, or an existing lwIP interface name. These are not the browser/SSH computer's interface names. If name resolution returns an unscoped link-local address, `nslookup` displays it, but ping/traceroute request an explicit scoped literal rather than guessing an interface. Numeric `nslookup` input is displayed without DNS/reverse lookup.
|
||||||
|
|
||||||
|
`nslookup` without a selector performs separate A and AAAA queries. The installed resolver returns one selected address per family (`CONFIG_LWIP_DNS_MAX_HOST_IP=1`), not a complete DNS RRset. Missing records, DNS timeouts and server failures can share the same SDK error; partial success is displayed rather than hidden. Unicast names require a usable configured DNS server; SLAAC alone does not provision one in this firmware. `.local` uses the existing lwIP mDNS query support. No DNS server or RA/DHCP policy is changed by these commands.
|
||||||
|
|
||||||
|
Traceroute sends one eight-byte echo probe per hop with a one-second absolute receive deadline; it can stop up to 999 microseconds early to avoid the SDK's zero-millisecond/infinite-wait conversion. Hop addresses are numeric. ICMPv6 replies are checked against the actual IDF raw framing/checksum, destination and probe identity; IPv6 extension headers/fragments in replies or quotes are unsupported and ignored. Filtering/rate limiting can produce `*` even when ordinary connectivity works. DNS, scheduler and send latency are outside the receive deadline.
|
||||||
|
|
||||||
|
Ping retains the SDK's bounded-count transient task and existing PSRAM event queue. Its console wait is bounded to `count * 2 + 2` seconds after start, but unrelated ICMP can extend the SDK receive loop: after a console timeout, further pings are refused until the old END event arrives. END finishes callback production; SDK resource teardown may briefly overlap a subsequent session. No IPv6 TTL is printed because the installed SDK does not provide a valid received IPv6 hop-limit profile. These commands serialize the administration dispatcher while executing; they do not run on HTTPD or change the broker/serial path.
|
||||||
|
|
||||||
|
Focused host/SDK and surface tests: `tests/network_diagnostics/` and `tests/network_diagnostics_surfaces/`. Real IPv4/IPv6/scoped ping, DNS and multi-hop traceroute remain device-validation tasks for this follow-up.
|
||||||
|
|
||||||
## mDNS
|
## mDNS
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
@@ -125,7 +151,7 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|
|||||||
|
|
||||||
When the Wi-Fi station has IPv4 or a preferred IPv6 address, the Wi-Fi manager announces `sak-<suffix>.local` and advertises available HTTPS/SSH services through DNS-SD. This is STA-only, local-link discovery, not certificate or host-key trust. The default suffix is the lower-case hexadecimal STA MAC address. Suffixes may contain lowercase ASCII letters, digits, and internal hyphens only. Changing a suffix queues a best-effort reannouncement and updates the DHCPv4 hostname (`sak-<suffix>`, without `.local`) for subsequent DHCP exchanges; it does not force a lease restart. A configured DHCP/DNS server may publish that name in its own zone.
|
When the Wi-Fi station has IPv4 or a preferred IPv6 address, the Wi-Fi manager announces `sak-<suffix>.local` and advertises available HTTPS/SSH services through DNS-SD. This is STA-only, local-link discovery, not certificate or host-key trust. The default suffix is the lower-case hexadecimal STA MAC address. Suffixes may contain lowercase ASCII letters, digits, and internal hyphens only. Changing a suffix queues a best-effort reannouncement and updates the DHCPv4 hostname (`sak-<suffix>`, without `.local`) for subsequent DHCP exchanges; it does not force a lease restart. A configured DHCP/DNS server may publish that name in its own zone.
|
||||||
|
|
||||||
`wifi status` and browser Network status distinguish IPv4 absence and IPv6 link-local/ULA/GUA availability. `wifi` / `wifi status` and the web Network settings dialog additionally list up to three actual preferred IPv6 addresses, labelled link-local, ULA or GUA; these are copied with the flags, not inferred from enabled IPv6 support. A link-local destination needs the client's interface as its zone. To separate client address preference from IPv6 reachability, run `ping -6 -c 3 sak-1024.local` on an IPv6-capable client; choosing IPv4 with plain `ping` does not mean the device lacks IPv6. If lookup fails, inspect AAAA resolution (for example `avahi-resolve-host-name -6 sak-1024.local` where Avahi is installed), then test the numeric address from `wifi status` directly. Link-local-only connectivity counts as `ONLINE` and can retire the fallback AP after the existing stability interval; it does not establish Internet access. HTTPS/WebSocket and SSH support both families; browser IPv6 literals require brackets and cannot contain interface zones. Use the `.local` hostname for link-local browser access where supported by the client. Existing ping selects the first usable resolver result without family racing, and traceroute remains IPv4-only. mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access. See [Phase 12](roadmap.md#phase-12--advanced-network-integration) for discovery convergence limits and pending device validation.
|
`wifi status` and browser Network status distinguish IPv4 absence and IPv6 link-local/ULA/GUA availability. `wifi` / `wifi status` and the web Network settings dialog additionally list up to three actual preferred IPv6 addresses, labelled link-local, ULA or GUA; these are copied with the flags, not inferred from enabled IPv6 support. A link-local destination needs the client's interface as its zone. To separate client address preference from IPv6 reachability, run `ping -6 -c 3 sak-1024.local` on an IPv6-capable client; choosing IPv4 with plain `ping` does not mean the device lacks IPv6. If lookup fails, inspect AAAA resolution (for example `avahi-resolve-host-name -6 sak-1024.local` where Avahi is installed), then test the numeric address from `wifi status` directly. Link-local-only connectivity counts as `ONLINE` and can retire the fallback AP after the existing stability interval; it does not establish Internet access. HTTPS/WebSocket and SSH support both families; browser IPv6 literals require brackets and cannot contain interface zones. Use the `.local` hostname for link-local browser access where supported by the client. On-device diagnostics support explicit family selection and scoped IPv6 as described above; Linux client syntax and interface scope are separate. mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access. See [Phase 12](roadmap.md#phase-12--advanced-network-integration) for discovery convergence limits and recorded user acceptance.
|
||||||
|
|
||||||
## HTTPS web terminal
|
## HTTPS web terminal
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -314,7 +314,7 @@ The device is not intended to become a general-purpose router. Captive-portal in
|
|||||||
- mDNS 1.12.0 exposes no readiness acknowledgement and can silently drop queued family actions. Absent families are disabled on each one-second owner pass; available families are re-enabled/reprobed on a 30-second repair cadence. Upstream synchronous calls can delay this cadence. There can be a transient stale/zero-A response window after IPv4 loss before disable is processed. AAAA records follow upstream **valid-address** semantics, including deprecated-but-still-valid addresses, not preferred-only filtering. Client caches expire independently.
|
- mDNS 1.12.0 exposes no readiness acknowledgement and can silently drop queued family actions. Absent families are disabled on each one-second owner pass; available families are re-enabled/reprobed on a 30-second repair cadence. Upstream synchronous calls can delay this cadence. There can be a transient stale/zero-A response window after IPv4 loss before disable is processed. AAAA records follow upstream **valid-address** semantics, including deprecated-but-still-valid addresses, not preferred-only filtering. Client caches expire independently.
|
||||||
- `cmake/mdns_membership.*` applies one version/hash-guarded, build-local source overlay to mDNS 1.12.0: balance per-family multicast leaves and unwind joins after PCB creation failure. Managed sources and dependency versions remain unchanged. An upstream mismatch fails configuration for deliberate review; see `tests/mdns_membership/README.md` for maintenance/removal and regression evidence. This is not the abandoned Phase 9 patch set.
|
- `cmake/mdns_membership.*` applies one version/hash-guarded, build-local source overlay to mDNS 1.12.0: balance per-family multicast leaves and unwind joins after PCB creation failure. Managed sources and dependency versions remain unchanged. An upstream mismatch fails configuration for deliberate review; see `tests/mdns_membership/README.md` for maintenance/removal and regression evidence. This is not the abandoned Phase 9 patch set.
|
||||||
- `sdkconfig.defaults` explicitly enables IPv4/IPv6 and increases mDNS service capacity from one to two. Existing saved configurations override defaults: verify `CONFIG_LWIP_IPV4=y`, `CONFIG_LWIP_IPV6=y`, `CONFIG_MDNS_MAX_SERVICES=2` and STA-only predefined mDNS interfaces before building. The local N16R8 saved configuration was updated accordingly. No new task, transport socket, broker slot, serial buffer, partition, filesystem or generated web asset was added.
|
- `sdkconfig.defaults` explicitly enables IPv4/IPv6 and increases mDNS service capacity from one to two. Existing saved configurations override defaults: verify `CONFIG_LWIP_IPV4=y`, `CONFIG_LWIP_IPV6=y`, `CONFIG_MDNS_MAX_SERVICES=2` and STA-only predefined mDNS interfaces before building. The local N16R8 saved configuration was updated accordingly. No new task, transport socket, broker slot, serial buffer, partition, filesystem or generated web asset was added.
|
||||||
- Existing diagnostic limits remain: hostname ping selects the resolver's first usable result (not Happy Eyeballs), scoped link-local ping is not newly supported, and traceroute remains IPv4-only. DHCPv6, infrastructure DNS registration of SLAAC addresses, and new IPv6 resolver provisioning are outside this baseline.
|
- The accepted baseline initially retained IPv4-only traceroute and limited family/scope selection. The later requested [dual-stack diagnostics follow-up](command_reference.md#dual-stack-network-diagnostics) adds `-4`/`-6` to ping/traceroute/nslookup across UART0, admin SSH and browser Admin, scoped IPv6 probes and explicit A+AAAA lookup. This follow-up is built/host-tested, not covered retroactively by the acceptance capture below. DHCPv6, infrastructure DNS registration of SLAAC addresses, and new IPv6 resolver provisioning remain outside scope.
|
||||||
|
|
||||||
#### Validation evidence and acceptance
|
#### Validation evidence and acceptance
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ HTTPD alone owns browser-admin socket IO and its 1,552-byte PSRAM-only payload.
|
|||||||
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
|
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
|
||||||
|
|
||||||
- Browser `web` permits only `web status`, `web stop`, and exact `web certificate rotate --force`; certificate info/reset, diagnostics/performance and other web forms are denied.
|
- Browser `web` permits only `web status`, `web stop`, and exact `web certificate rotate --force`; certificate info/reset, diagnostics/performance and other web forms are denied.
|
||||||
- Browser `wifi` uses the canonical command handler, including bare `wifi` (status), profile/AP settings, persistence, lifecycle and network diagnostics. Profile/AP secret entry uses the shared hidden prompt: no input echo, history or completion of secret bytes; cancellation, disconnect and failed currentness discard the input. Explicit `wifi ap show-secret` reveals the AP password only in the invoking admin terminal; routine status and completion do not reveal credentials. Browser `mdns` still permits only exact `mdns status`.
|
- Browser `wifi` uses the canonical command handler, including bare `wifi` (status), profile/AP settings, persistence, lifecycle and network diagnostics. Profile/AP secret entry uses the shared hidden prompt: no input echo, history or completion of secret bytes; cancellation, disconnect and failed currentness discard the input. Explicit `wifi ap show-secret` reveals the AP password only in the invoking admin terminal; routine status and completion do not reveal credentials. Browser `mdns` still permits only exact `mdns status`. Root and `wifi`-prefixed ping/traceroute/nslookup support the same `-4`/`-6` and device-local IPv6 scope syntax as UART0/admin SSH; see [dual-stack diagnostics](command_reference.md#dual-stack-network-diagnostics). They run on the shared dispatcher, not HTTPD or the client browser; no extra endpoint or permission expansion is involved.
|
||||||
- Browser `user` permits status/list/show and interactive add/password plus forced role/delete for **other accounts only**. Self changes, generated passwords, key commands and recovery are denied there; typed Accounts supports the separately bounded self/generated/key workflows.
|
- Browser `user` permits status/list/show and interactive add/password plus forced role/delete for **other accounts only**. Self changes, generated passwords, key commands and recovery are denied there; typed Accounts supports the separately bounded self/generated/key workflows.
|
||||||
- Browser SSH stop/disconnect/reset and host-key mutation are denied; typed SSH Settings has its own safe owner path. Do not claim full browser-shell parity.
|
- Browser SSH stop/disconnect/reset and host-key mutation are denied; typed SSH Settings has its own safe owner path. Do not claim full browser-shell parity.
|
||||||
- Browser `reboot` and owner-relative `exit` are supported. First-admin provisioning uses normal `user add` on UART0; unavailable-database recovery is UART0-only. The legacy `user bootstrap` and web credential commands no longer exist.
|
- Browser `reboot` and owner-relative `exit` are supported. First-admin provisioning uses normal `user add` on UART0; unavailable-database recovery is UART0-only. The legacy `user bootstrap` and web credential commands no longer exist.
|
||||||
|
|||||||
@@ -164,8 +164,22 @@ static const char *const s_completion_candidates[] = {
|
|||||||
"wifi ap secret",
|
"wifi ap secret",
|
||||||
"wifi ap show-secret",
|
"wifi ap show-secret",
|
||||||
"wifi ping",
|
"wifi ping",
|
||||||
|
"wifi ping -4",
|
||||||
|
"wifi ping -6",
|
||||||
"wifi nslookup",
|
"wifi nslookup",
|
||||||
|
"wifi nslookup -4",
|
||||||
|
"wifi nslookup -6",
|
||||||
"wifi traceroute",
|
"wifi traceroute",
|
||||||
|
"wifi traceroute -4",
|
||||||
|
"wifi traceroute -6",
|
||||||
|
|
||||||
|
/* Shared diagnostics: fixed family flags only, never guessed hosts. */
|
||||||
|
"ping -4",
|
||||||
|
"ping -6",
|
||||||
|
"nslookup -4",
|
||||||
|
"nslookup -6",
|
||||||
|
"traceroute -4",
|
||||||
|
"traceroute -6",
|
||||||
|
|
||||||
/* Station mDNS hostname configuration. */
|
/* Station mDNS hostname configuration. */
|
||||||
"mdns status",
|
"mdns status",
|
||||||
|
|||||||
+428
-244
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
#include <net/if.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_idf_version.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
@@ -21,6 +24,7 @@
|
|||||||
#include "lwip/inet_chksum.h"
|
#include "lwip/inet_chksum.h"
|
||||||
#include "lwip/ip_addr.h"
|
#include "lwip/ip_addr.h"
|
||||||
#include "lwip/netdb.h"
|
#include "lwip/netdb.h"
|
||||||
|
#include "lwip/netif.h"
|
||||||
#include "lwip/prot/icmp.h"
|
#include "lwip/prot/icmp.h"
|
||||||
#include "lwip/prot/ip4.h"
|
#include "lwip/prot/ip4.h"
|
||||||
#include "lwip/sockets.h"
|
#include "lwip/sockets.h"
|
||||||
@@ -35,21 +39,25 @@
|
|||||||
#define TRACEROUTE_TIMEOUT_US INT64_C(1000000)
|
#define TRACEROUTE_TIMEOUT_US INT64_C(1000000)
|
||||||
#define NUMERIC_ADDRESS_CAPACITY 48U
|
#define NUMERIC_ADDRESS_CAPACITY 48U
|
||||||
|
|
||||||
/* This covers two maximum-size IPv4 headers plus both required ICMP headers. */
|
/* Raw receive framing and IP_TTL-for-IPv6 are SDK-specific; re-audit on upgrade. */
|
||||||
#define TRACEROUTE_REPLY_CAPACITY \
|
#if ESP_IDF_VERSION != ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||||
(IP_HLEN_MAX + sizeof(struct icmp_hdr) + IP_HLEN_MAX + sizeof(struct icmp_echo_hdr))
|
#error "Re-audit network diagnostics raw sockets for this ESP-IDF version"
|
||||||
|
#endif
|
||||||
|
#define TRACEROUTE_REPLY_CAPACITY 1280U
|
||||||
|
|
||||||
static void print_command_usage(const char *command)
|
static void print_command_usage(const char *command)
|
||||||
{
|
{
|
||||||
if (command != NULL && strcmp(command, "ping") == 0) {
|
if (command != NULL && strcmp(command, "ping") == 0) {
|
||||||
printf("Usage: ping <host> [count] (count: 1..20, default: 4)\n");
|
printf("Usage: ping [-4|-6] <host> [count] (count: 1..20, default: 4)\n");
|
||||||
} else if (command != NULL && strcmp(command, "nslookup") == 0) {
|
} else if (command != NULL && strcmp(command, "nslookup") == 0) {
|
||||||
printf("Usage: nslookup <host>\n");
|
printf("Usage: nslookup [-4|-6] <host>\n");
|
||||||
} else if (command != NULL && strcmp(command, "traceroute") == 0) {
|
} else if (command != NULL && strcmp(command, "traceroute") == 0) {
|
||||||
printf("Usage: traceroute <host> [max-hops] (IPv4 only; 1..30, default: 16)\n");
|
printf("Usage: traceroute [-4|-6] <host> [max-hops] (1..30, default: 16)\n");
|
||||||
} else {
|
} else {
|
||||||
printf("Network commands: ping, nslookup, traceroute\n");
|
printf("Network commands: ping, nslookup, traceroute\n");
|
||||||
}
|
}
|
||||||
|
printf("One -4 or -6 may appear before or after the host; defaults prefer IPv4 for probes.\n"
|
||||||
|
"Link-local IPv6 probes require %%sta, %%ap, or a device interface index/name.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool parse_bounded_u32(const char *text, uint32_t minimum,
|
static bool parse_bounded_u32(const char *text, uint32_t minimum,
|
||||||
@@ -97,8 +105,19 @@ static bool sockaddr_to_numeric(const struct sockaddr *address,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return inet_ntop(address->sa_family, numeric_address, buffer,
|
if (inet_ntop(address->sa_family, numeric_address, buffer,
|
||||||
(socklen_t)buffer_size) != NULL;
|
(socklen_t)buffer_size) == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (address->sa_family == AF_INET6) {
|
||||||
|
uint32_t zone = ((const struct sockaddr_in6 *)address)->sin6_scope_id;
|
||||||
|
if (zone != 0U) {
|
||||||
|
size_t used = strlen(buffer);
|
||||||
|
int written = snprintf(buffer + used, buffer_size - used, "%%%" PRIu32, zone);
|
||||||
|
return written >= 0 && (size_t)written < buffer_size - used;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool addrinfo_to_ip_addr(const struct addrinfo *entry, ip_addr_t *target)
|
static bool addrinfo_to_ip_addr(const struct addrinfo *entry, ip_addr_t *target)
|
||||||
@@ -124,6 +143,7 @@ static bool addrinfo_to_ip_addr(const struct addrinfo *entry, ip_addr_t *target)
|
|||||||
(const struct sockaddr_in6 *)entry->ai_addr;
|
(const struct sockaddr_in6 *)entry->ai_addr;
|
||||||
ip6_addr_t ipv6;
|
ip6_addr_t ipv6;
|
||||||
inet6_addr_to_ip6addr(&ipv6, &socket_address->sin6_addr);
|
inet6_addr_to_ip6addr(&ipv6, &socket_address->sin6_addr);
|
||||||
|
ip6_addr_set_zone(&ipv6, socket_address->sin6_scope_id);
|
||||||
ip_addr_copy_from_ip6(*target, ipv6);
|
ip_addr_copy_from_ip6(*target, ipv6);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -132,34 +152,178 @@ static bool addrinfo_to_ip_addr(const struct addrinfo *entry, ip_addr_t *target)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int resolve_ping_target(const char *host, ip_addr_t *target,
|
typedef struct {
|
||||||
char *numeric, size_t numeric_size)
|
const char *host;
|
||||||
|
int family;
|
||||||
|
uint32_t limit;
|
||||||
|
} diagnostic_arguments_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct sockaddr_storage address;
|
||||||
|
socklen_t length;
|
||||||
|
} diagnostic_target_t;
|
||||||
|
|
||||||
|
static bool parse_arguments(int argc, char **argv, uint32_t default_limit,
|
||||||
|
uint32_t maximum, diagnostic_arguments_t *args)
|
||||||
{
|
{
|
||||||
struct addrinfo hints = {
|
*args = (diagnostic_arguments_t){.family = AF_UNSPEC, .limit = default_limit};
|
||||||
.ai_family = AF_UNSPEC,
|
bool have_limit = false;
|
||||||
.ai_socktype = SOCK_RAW,
|
for (int i = 1; i < argc; ++i) {
|
||||||
};
|
const char *word = argv[i];
|
||||||
struct addrinfo *results = NULL;
|
if (word == NULL || *word == '\0') {
|
||||||
int resolver_result = getaddrinfo(host, NULL, &hints, &results);
|
return false;
|
||||||
if (resolver_result != 0) {
|
}
|
||||||
printf("ping: could not resolve '%s' (getaddrinfo error %d)\n",
|
if (*word == '-') {
|
||||||
host, resolver_result);
|
if (args->family != AF_UNSPEC ||
|
||||||
|
(strcmp(word, "-4") != 0 && strcmp(word, "-6") != 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
args->family = word[1] == '4' ? AF_INET : AF_INET6;
|
||||||
|
} else if (args->host == NULL) {
|
||||||
|
args->host = word;
|
||||||
|
} else if (have_limit || maximum == 0U ||
|
||||||
|
!parse_bounded_u32(word, 1U, maximum, &args->limit)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
have_limit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args->host != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned int device_zone(const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp(name, "sta") == 0 || strcmp(name, "ap") == 0) {
|
||||||
|
esp_netif_t *netif = esp_netif_get_handle_from_ifkey(
|
||||||
|
strcmp(name, "sta") == 0 ? "WIFI_STA_DEF" : "WIFI_AP_DEF");
|
||||||
|
int index = netif != NULL ? esp_netif_get_netif_impl_index(netif) : 0;
|
||||||
|
return index > 0 && index <= UINT8_MAX ? (unsigned int)index : 0U;
|
||||||
|
}
|
||||||
|
uint32_t index;
|
||||||
|
char interface_name[IF_NAMESIZE];
|
||||||
|
if (parse_bounded_u32(name, 1U, UINT8_MAX, &index)) {
|
||||||
|
return if_indextoname(index, interface_name) != NULL ? index : 0U;
|
||||||
|
}
|
||||||
|
return if_nametoindex(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return 1 for a literal, 0 for a hostname, -1 for invalid literals.
|
||||||
|
* Missing link-local zones are valid for display, but not for probing.
|
||||||
|
* Never pass '%' through DNS: zones belong to this device, not the client. */
|
||||||
|
static int parse_literal(const char *host, int family, diagnostic_target_t *target)
|
||||||
|
{
|
||||||
|
char address[INET6_ADDRSTRLEN];
|
||||||
|
const char *percent = strchr(host, '%');
|
||||||
|
size_t length = percent != NULL ? (size_t)(percent - host) : strlen(host);
|
||||||
|
if (length >= sizeof(address)) {
|
||||||
|
return percent != NULL || strchr(host, ':') != NULL ? -1 : 0;
|
||||||
|
}
|
||||||
|
memcpy(address, host, length);
|
||||||
|
address[length] = '\0';
|
||||||
|
memset(target, 0, sizeof(*target));
|
||||||
|
struct sockaddr_in *v4 = (struct sockaddr_in *)&target->address;
|
||||||
|
if (inet_pton(AF_INET, address, &v4->sin_addr) == 1) {
|
||||||
|
if (percent != NULL || family == AF_INET6) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
v4->sin_family = AF_INET;
|
||||||
|
target->length = sizeof(*v4);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)&target->address;
|
||||||
|
if (inet_pton(AF_INET6, address, &v6->sin6_addr) != 1) {
|
||||||
|
return percent != NULL || strchr(host, ':') != NULL ? -1 : 0;
|
||||||
|
}
|
||||||
|
if (family == AF_INET || IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
v6->sin6_family = AF_INET6;
|
||||||
|
target->length = sizeof(*v6);
|
||||||
|
if (percent != NULL) {
|
||||||
|
v6->sin6_scope_id = device_zone(percent + 1);
|
||||||
|
if (v6->sin6_scope_id == 0U) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
bool found = false;
|
static bool target_needs_scope(const diagnostic_target_t *target)
|
||||||
|
{
|
||||||
|
if (target->address.ss_family != AF_INET6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const struct sockaddr_in6 *v6 = (const struct sockaddr_in6 *)&target->address;
|
||||||
|
return IN6_IS_ADDR_LINKLOCAL(&v6->sin6_addr) && v6->sin6_scope_id == 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_scope_requirement(const char *numeric)
|
||||||
|
{
|
||||||
|
printf("Link-local %s needs an explicit device scope for probes: %s%%sta or %s%%ap\n"
|
||||||
|
"(choose the device interface, not the SSH/browser client's; a valid device index/name also works).\n",
|
||||||
|
numeric, numeric, numeric);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int resolve_family(const char *host, int family, diagnostic_target_t *target)
|
||||||
|
{
|
||||||
|
struct addrinfo hints = {.ai_family = family, .ai_socktype = SOCK_RAW};
|
||||||
|
struct addrinfo *results = NULL;
|
||||||
|
int error = getaddrinfo(host, NULL, &hints, &results);
|
||||||
|
if (error != 0) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = EAI_NONAME;
|
||||||
for (const struct addrinfo *entry = results; entry != NULL; entry = entry->ai_next) {
|
for (const struct addrinfo *entry = results; entry != NULL; entry = entry->ai_next) {
|
||||||
if (addrinfo_to_ip_addr(entry, target) &&
|
size_t required = family == AF_INET ? sizeof(struct sockaddr_in) :
|
||||||
sockaddr_to_numeric(entry->ai_addr, entry->ai_addrlen,
|
sizeof(struct sockaddr_in6);
|
||||||
numeric, numeric_size)) {
|
if (entry->ai_family != family || entry->ai_addr == NULL ||
|
||||||
found = true;
|
entry->ai_addr->sa_family != family || entry->ai_addrlen < required) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
memset(target, 0, sizeof(*target));
|
||||||
|
memcpy(&target->address, entry->ai_addr, required);
|
||||||
|
target->length = required;
|
||||||
|
if (family == AF_INET6) {
|
||||||
|
const struct sockaddr_in6 *v6 = (const struct sockaddr_in6 *)&target->address;
|
||||||
|
if (IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
freeaddrinfo(results);
|
freeaddrinfo(results);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
if (!found) {
|
static int resolve_target(const diagnostic_arguments_t *args, diagnostic_target_t *target,
|
||||||
printf("ping: '%s' did not resolve to a supported IPv4 or IPv6 address\n", host);
|
char *numeric, size_t numeric_size)
|
||||||
|
{
|
||||||
|
int literal = parse_literal(args->host, args->family, target);
|
||||||
|
if (literal < 0) {
|
||||||
|
printf("Invalid address/family/zone. Link-local IPv6 requires %%sta, %%ap, or a valid\n"
|
||||||
|
"device interface index/name (not the SSH/browser client's interface).\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (literal == 0) {
|
||||||
|
int first = args->family == AF_UNSPEC ? AF_INET : args->family;
|
||||||
|
int error = resolve_family(args->host, first, target);
|
||||||
|
/* IDF collapses DNS absence, timeout and server failure into EAI_FAIL.
|
||||||
|
* Fall back only after resolution returned no address, never after IO. */
|
||||||
|
if (args->family == AF_UNSPEC && (error == EAI_NONAME || error == EAI_FAIL)) {
|
||||||
|
error = resolve_family(args->host, AF_INET6, target);
|
||||||
|
}
|
||||||
|
if (error != 0) {
|
||||||
|
printf("Could not resolve '%s' (getaddrinfo error %d).\n", args->host, error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sockaddr_to_numeric((const struct sockaddr *)&target->address, target->length,
|
||||||
|
numeric, numeric_size)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (target_needs_scope(target)) {
|
||||||
|
print_scope_requirement(numeric);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -191,6 +355,11 @@ static StaticQueue_t s_ping_queue_storage;
|
|||||||
* storage cannot dangle. Queue control stays internal. No internal-RAM fallback. */
|
* storage cannot dangle. Queue control stays internal. No internal-RAM fallback. */
|
||||||
static uint8_t *s_ping_queue_bytes;
|
static uint8_t *s_ping_queue_bytes;
|
||||||
static QueueHandle_t s_ping_queue;
|
static QueueHandle_t s_ping_queue;
|
||||||
|
/* SDK ping deletion is asynchronous. Keep callback context alive even if the
|
||||||
|
* console deadline expires; END permits callback queue reuse, not proof that
|
||||||
|
* the retiring SDK task/socket has been destroyed. */
|
||||||
|
static ping_wait_context_t s_ping_context;
|
||||||
|
static bool s_ping_pending;
|
||||||
|
|
||||||
static void ping_on_success(esp_ping_handle_t handle, void *arguments)
|
static void ping_on_success(esp_ping_handle_t handle, void *arguments)
|
||||||
{
|
{
|
||||||
@@ -274,21 +443,35 @@ static void ping_on_end(esp_ping_handle_t handle, void *arguments)
|
|||||||
|
|
||||||
static int execute_ping(int argc, char **argv)
|
static int execute_ping(int argc, char **argv)
|
||||||
{
|
{
|
||||||
if ((argc != 2 && argc != 3) || argv[1] == NULL || *argv[1] == '\0') {
|
if (s_ping_pending) {
|
||||||
|
ping_event_t pending;
|
||||||
|
while (xQueueReceive(s_ping_queue, &pending, 0U) == pdTRUE) {
|
||||||
|
if (pending.kind == PING_EVENT_END) {
|
||||||
|
s_ping_pending = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s_ping_pending) {
|
||||||
|
printf("ping: previous SDK session has not completed; no new session started\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diagnostic_arguments_t args;
|
||||||
|
if (!parse_arguments(argc, argv, PING_DEFAULT_COUNT, PING_MAX_COUNT, &args)) {
|
||||||
print_command_usage("ping");
|
print_command_usage("ping");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
uint32_t count = args.limit;
|
||||||
uint32_t count = PING_DEFAULT_COUNT;
|
diagnostic_target_t resolved;
|
||||||
if (argc == 3 &&
|
|
||||||
!parse_bounded_u32(argv[2], PING_MIN_COUNT, PING_MAX_COUNT, &count)) {
|
|
||||||
print_command_usage("ping");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ip_addr_t target;
|
|
||||||
char numeric[NUMERIC_ADDRESS_CAPACITY];
|
char numeric[NUMERIC_ADDRESS_CAPACITY];
|
||||||
if (resolve_ping_target(argv[1], &target, numeric, sizeof(numeric)) != 0) {
|
if (resolve_target(&args, &resolved, numeric, sizeof(numeric)) != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ip_addr_t target;
|
||||||
|
struct addrinfo entry = {.ai_family = resolved.address.ss_family,
|
||||||
|
.ai_addr = (struct sockaddr *)&resolved.address,
|
||||||
|
.ai_addrlen = resolved.length};
|
||||||
|
if (!addrinfo_to_ip_addr(&entry, &target)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,14 +494,20 @@ static int execute_ping(int argc, char **argv)
|
|||||||
printf("ping: could not allocate event queue\n");
|
printf("ping: could not allocate event queue\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
ping_wait_context_t context = {.queue = s_ping_queue};
|
s_ping_context.queue = s_ping_queue;
|
||||||
|
|
||||||
esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
|
esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
|
||||||
config.count = count;
|
config.count = count;
|
||||||
|
config.interval_ms = 1000U;
|
||||||
|
config.timeout_ms = 1000U;
|
||||||
config.target_addr = target;
|
config.target_addr = target;
|
||||||
|
if (resolved.address.ss_family == AF_INET6) {
|
||||||
|
/* IDF 5.5 ping omits sin6_scope_id when constructing sendto's address. */
|
||||||
|
config.interface = ((const struct sockaddr_in6 *)&resolved.address)->sin6_scope_id;
|
||||||
|
}
|
||||||
|
|
||||||
const esp_ping_callbacks_t callbacks = {
|
const esp_ping_callbacks_t callbacks = {
|
||||||
.cb_args = &context,
|
.cb_args = &s_ping_context,
|
||||||
.on_ping_success = ping_on_success,
|
.on_ping_success = ping_on_success,
|
||||||
.on_ping_timeout = ping_on_timeout,
|
.on_ping_timeout = ping_on_timeout,
|
||||||
.on_ping_end = ping_on_end,
|
.on_ping_end = ping_on_end,
|
||||||
@@ -331,24 +520,32 @@ static int execute_ping(int argc, char **argv)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("PING %s (%s): %" PRIu32 " probes\n", argv[1], numeric, count);
|
printf("PING %s (%s): %" PRIu32 " probes\n", args.host, numeric, count);
|
||||||
|
s_ping_pending = true;
|
||||||
error = esp_ping_start(session);
|
error = esp_ping_start(session);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
printf("ping: could not start session: %s\n", esp_err_to_name(error));
|
printf("ping: could not start session: %s\n", esp_err_to_name(error));
|
||||||
(void)esp_ping_delete_session(session);
|
(void)esp_ping_delete_session(session);
|
||||||
|
s_ping_pending = false;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int64_t deadline = esp_timer_get_time() + ((int64_t)count * 2000 + 2000) * 1000;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
ping_event_t event;
|
ping_event_t event;
|
||||||
if (xQueueReceive(s_ping_queue, &event, portMAX_DELAY) != pdTRUE) {
|
int64_t remaining = deadline - esp_timer_get_time();
|
||||||
printf("ping: wait for session completion failed\n");
|
TickType_t wait = remaining > 0 ? pdMS_TO_TICKS((uint32_t)((remaining + 999) / 1000)) : 0;
|
||||||
|
if (remaining <= 0 || xQueueReceive(s_ping_queue, &event, wait ? wait : 1U) != pdTRUE) {
|
||||||
|
/* Do not touch the handle here: END may concurrently delete it.
|
||||||
|
* SDK receive may outlive its configured timeout under ICMP noise. */
|
||||||
|
printf("ping: console deadline exceeded; SDK session retained until completion\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (event.kind == PING_EVENT_LINE) {
|
if (event.kind == PING_EVENT_LINE) {
|
||||||
printf("%s\n", event.line);
|
printf("%s\n", event.line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
s_ping_pending = false;
|
||||||
if (event.profile_error != ESP_OK) {
|
if (event.profile_error != ESP_OK) {
|
||||||
printf("ping: session ended, but summary profile retrieval failed: %s\n",
|
printf("ping: session ended, but summary profile retrieval failed: %s\n",
|
||||||
esp_err_to_name(event.profile_error));
|
esp_err_to_name(event.profile_error));
|
||||||
@@ -371,134 +568,53 @@ static int execute_ping(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool socket_addresses_equal(const struct addrinfo *left,
|
|
||||||
const struct addrinfo *right)
|
|
||||||
{
|
|
||||||
if (left == NULL || right == NULL || left->ai_addr == NULL ||
|
|
||||||
right->ai_addr == NULL || left->ai_family != right->ai_family) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (left->ai_family == AF_INET &&
|
|
||||||
left->ai_addrlen >= (socklen_t)sizeof(struct sockaddr_in) &&
|
|
||||||
right->ai_addrlen >= (socklen_t)sizeof(struct sockaddr_in)) {
|
|
||||||
const struct sockaddr_in *left_address =
|
|
||||||
(const struct sockaddr_in *)left->ai_addr;
|
|
||||||
const struct sockaddr_in *right_address =
|
|
||||||
(const struct sockaddr_in *)right->ai_addr;
|
|
||||||
return left_address->sin_addr.s_addr == right_address->sin_addr.s_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(CONFIG_LWIP_IPV6) && CONFIG_LWIP_IPV6
|
|
||||||
if (left->ai_family == AF_INET6 &&
|
|
||||||
left->ai_addrlen >= (socklen_t)sizeof(struct sockaddr_in6) &&
|
|
||||||
right->ai_addrlen >= (socklen_t)sizeof(struct sockaddr_in6)) {
|
|
||||||
const struct sockaddr_in6 *left_address =
|
|
||||||
(const struct sockaddr_in6 *)left->ai_addr;
|
|
||||||
const struct sockaddr_in6 *right_address =
|
|
||||||
(const struct sockaddr_in6 *)right->ai_addr;
|
|
||||||
return memcmp(&left_address->sin6_addr, &right_address->sin6_addr,
|
|
||||||
sizeof(left_address->sin6_addr)) == 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool address_appeared_earlier(const struct addrinfo *first,
|
|
||||||
const struct addrinfo *current)
|
|
||||||
{
|
|
||||||
for (const struct addrinfo *entry = first;
|
|
||||||
entry != NULL && entry != current; entry = entry->ai_next) {
|
|
||||||
if (socket_addresses_equal(entry, current)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int execute_nslookup(int argc, char **argv)
|
static int execute_nslookup(int argc, char **argv)
|
||||||
{
|
{
|
||||||
if (argc != 2 || argv[1] == NULL || *argv[1] == '\0') {
|
diagnostic_arguments_t args;
|
||||||
|
if (!parse_arguments(argc, argv, 0U, 0U, &args)) {
|
||||||
print_command_usage("nslookup");
|
print_command_usage("nslookup");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
diagnostic_target_t target;
|
||||||
struct addrinfo hints = {
|
|
||||||
.ai_family = AF_UNSPEC,
|
|
||||||
.ai_socktype = SOCK_STREAM,
|
|
||||||
};
|
|
||||||
struct addrinfo *results = NULL;
|
|
||||||
int resolver_result = getaddrinfo(argv[1], NULL, &hints, &results);
|
|
||||||
if (resolver_result != 0) {
|
|
||||||
printf("nslookup: could not resolve '%s' (getaddrinfo error %d)\n",
|
|
||||||
argv[1], resolver_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Name: %s\n", argv[1]);
|
|
||||||
size_t printed = 0U;
|
|
||||||
for (const struct addrinfo *entry = results; entry != NULL; entry = entry->ai_next) {
|
|
||||||
if (address_appeared_earlier(results, entry)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
char numeric[NUMERIC_ADDRESS_CAPACITY];
|
char numeric[NUMERIC_ADDRESS_CAPACITY];
|
||||||
if (!sockaddr_to_numeric(entry->ai_addr, entry->ai_addrlen,
|
int literal = parse_literal(args.host, args.family, &target);
|
||||||
|
if (literal != 0) {
|
||||||
|
if (literal < 0 ||
|
||||||
|
!sockaddr_to_numeric((const struct sockaddr *)&target.address, target.length,
|
||||||
numeric, sizeof(numeric))) {
|
numeric, sizeof(numeric))) {
|
||||||
|
printf("nslookup: invalid address/family/device zone\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Address: %s (numeric literal; no DNS query)\n", numeric);
|
||||||
|
if (target_needs_scope(&target)) {
|
||||||
|
print_scope_requirement(numeric);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printf("Name: %s\nResolver: lwIP getaddrinfo; one selected address per family (configured max %d), not a full DNS RRset.\n"
|
||||||
|
"DNS absence, timeout and server failure may share EAI_FAIL.\n", args.host,
|
||||||
|
CONFIG_LWIP_DNS_MAX_HOST_IP);
|
||||||
|
unsigned int printed = 0U;
|
||||||
|
const int families[] = {AF_INET, AF_INET6};
|
||||||
|
for (size_t i = 0; i < 2U; ++i) {
|
||||||
|
int family = families[i];
|
||||||
|
if (args.family != AF_UNSPEC && args.family != family) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
int error = resolve_family(args.host, family, &target);
|
||||||
printf("Address: %s (%s)\n", numeric,
|
if (error == 0 && sockaddr_to_numeric((const struct sockaddr *)&target.address,
|
||||||
entry->ai_family == AF_INET ? "IPv4" : "IPv6");
|
target.length, numeric, sizeof(numeric))) {
|
||||||
|
printf("%s: %s\n", family == AF_INET ? "A" : "AAAA", numeric);
|
||||||
|
if (target_needs_scope(&target)) {
|
||||||
|
print_scope_requirement(numeric);
|
||||||
|
}
|
||||||
++printed;
|
++printed;
|
||||||
}
|
} else {
|
||||||
freeaddrinfo(results);
|
printf("%s: no usable address (resolver error %d; not proof of no record)\n",
|
||||||
|
family == AF_INET ? "A" : "AAAA", error);
|
||||||
if (printed == 0U) {
|
|
||||||
printf("nslookup: no supported IPv4 or IPv6 addresses returned\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int resolve_traceroute_target(const char *host,
|
|
||||||
struct sockaddr_in *target,
|
|
||||||
char *numeric, size_t numeric_size)
|
|
||||||
{
|
|
||||||
struct addrinfo hints = {
|
|
||||||
.ai_family = AF_INET,
|
|
||||||
.ai_socktype = SOCK_RAW,
|
|
||||||
.ai_protocol = IPPROTO_ICMP,
|
|
||||||
};
|
|
||||||
struct addrinfo *results = NULL;
|
|
||||||
int resolver_result = getaddrinfo(host, NULL, &hints, &results);
|
|
||||||
if (resolver_result != 0) {
|
|
||||||
printf("traceroute: could not resolve IPv4 host '%s' (getaddrinfo error %d)\n",
|
|
||||||
host, resolver_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const struct addrinfo *selected = NULL;
|
|
||||||
for (const struct addrinfo *entry = results; entry != NULL; entry = entry->ai_next) {
|
|
||||||
if (entry->ai_family == AF_INET && entry->ai_addr != NULL &&
|
|
||||||
entry->ai_addrlen >= (socklen_t)sizeof(struct sockaddr_in)) {
|
|
||||||
selected = entry;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return printed != 0U ? 0 : 1;
|
||||||
if (selected == NULL ||
|
|
||||||
!sockaddr_to_numeric(selected->ai_addr, selected->ai_addrlen,
|
|
||||||
numeric, numeric_size)) {
|
|
||||||
freeaddrinfo(results);
|
|
||||||
printf("traceroute: '%s' did not resolve to an IPv4 address\n", host);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(target, selected->ai_addr, sizeof(*target));
|
|
||||||
freeaddrinfo(results);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -508,85 +624,119 @@ typedef enum {
|
|||||||
TRACE_REPLY_UNREACHABLE,
|
TRACE_REPLY_UNREACHABLE,
|
||||||
} trace_reply_kind_t;
|
} trace_reply_kind_t;
|
||||||
|
|
||||||
|
static uint16_t read_be16(const uint8_t *bytes)
|
||||||
|
{
|
||||||
|
return (uint16_t)((uint16_t)bytes[0] << 8 | bytes[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t checksum_sum(const uint8_t *bytes, size_t length, uint32_t sum)
|
||||||
|
{
|
||||||
|
while (length >= 2U) {
|
||||||
|
sum += read_be16(bytes);
|
||||||
|
bytes += 2;
|
||||||
|
length -= 2;
|
||||||
|
}
|
||||||
|
if (length != 0U) {
|
||||||
|
sum += (uint16_t)bytes[0] << 8;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool checksum_valid(uint32_t sum)
|
||||||
|
{
|
||||||
|
while (sum >> 16) {
|
||||||
|
sum = (sum & 0xffffU) + (sum >> 16);
|
||||||
|
}
|
||||||
|
return sum == 0xffffU;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IDF raw sockets include the outer IP header and deliver before ICMP checksum
|
||||||
|
* validation. Reject all IPv6 extension headers (including fragments), both
|
||||||
|
* outer and quoted: the installed raw demux only matches the base next-header.
|
||||||
|
* No unaligned structure loads and no inspection beyond the received bytes. */
|
||||||
static trace_reply_kind_t parse_trace_reply(const uint8_t *packet, size_t length,
|
static trace_reply_kind_t parse_trace_reply(const uint8_t *packet, size_t length,
|
||||||
|
const diagnostic_target_t *target,
|
||||||
uint16_t expected_id,
|
uint16_t expected_id,
|
||||||
uint16_t expected_sequence,
|
uint16_t expected_sequence,
|
||||||
uint32_t expected_destination,
|
|
||||||
uint8_t *unreachable_code)
|
uint8_t *unreachable_code)
|
||||||
{
|
{
|
||||||
if (packet == NULL || length < IP_HLEN + sizeof(struct icmp_hdr)) {
|
bool v6 = target->address.ss_family == AF_INET6;
|
||||||
|
size_t header = v6 ? 40U : 20U;
|
||||||
|
if (packet == NULL || length < header + 8U || packet[0] >> 4 != (v6 ? 6 : 4)) {
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
const uint8_t *destination;
|
||||||
const struct ip_hdr *outer_ip = (const struct ip_hdr *)(const void *)packet;
|
size_t address_size;
|
||||||
size_t outer_header_length = IPH_HL_BYTES(outer_ip);
|
size_t source_offset;
|
||||||
if (IPH_V(outer_ip) != 4U || outer_header_length < IP_HLEN ||
|
size_t destination_offset;
|
||||||
outer_header_length > length ||
|
size_t total;
|
||||||
length - outer_header_length < sizeof(struct icmp_hdr) ||
|
if (v6) {
|
||||||
IPH_PROTO(outer_ip) != IPPROTO_ICMP) {
|
destination = (const uint8_t *)&((const struct sockaddr_in6 *)&target->address)->sin6_addr;
|
||||||
|
address_size = 16U;
|
||||||
|
source_offset = 8U;
|
||||||
|
destination_offset = 24U;
|
||||||
|
total = 40U + read_be16(packet + 4);
|
||||||
|
if (packet[6] != IPPROTO_ICMPV6 || total > length || total < 48U ||
|
||||||
|
!checksum_valid(checksum_sum(packet + 8, 32,
|
||||||
|
checksum_sum(packet + 40, total - 40, (uint32_t)(total - 40) + IPPROTO_ICMPV6)))) {
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
uint16_t outer_total_length = lwip_ntohs(IPH_LEN(outer_ip));
|
destination = (const uint8_t *)&((const struct sockaddr_in *)&target->address)->sin_addr;
|
||||||
if (outer_total_length < outer_header_length + sizeof(struct icmp_hdr)) {
|
address_size = 4U;
|
||||||
|
source_offset = 12U;
|
||||||
|
destination_offset = 16U;
|
||||||
|
header = (packet[0] & 15U) * 4U;
|
||||||
|
total = read_be16(packet + 2);
|
||||||
|
if (header < 20U || total > length || total < header + 8U ||
|
||||||
|
packet[9] != IPPROTO_ICMP || (read_be16(packet + 6) & 0x3fffU) != 0U ||
|
||||||
|
!checksum_valid(checksum_sum(packet, header, 0)) ||
|
||||||
|
!checksum_valid(checksum_sum(packet + header, total - header, 0))) {
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
size_t available = length;
|
|
||||||
if ((size_t)outer_total_length < available) {
|
|
||||||
available = outer_total_length;
|
|
||||||
}
|
}
|
||||||
|
const uint8_t *icmp = packet + header;
|
||||||
const uint8_t *outer_icmp_bytes = packet + outer_header_length;
|
if (icmp[0] == (v6 ? 129U : ICMP_ER)) {
|
||||||
const struct icmp_hdr *outer_icmp =
|
if (icmp[1] == 0U && memcmp(icmp + 4, &expected_id, 2) == 0 &&
|
||||||
(const struct icmp_hdr *)(const void *)outer_icmp_bytes;
|
memcmp(icmp + 6, &expected_sequence, 2) == 0 &&
|
||||||
|
memcmp(packet + source_offset, destination, address_size) == 0) {
|
||||||
if (ICMPH_TYPE(outer_icmp) == ICMP_ER) {
|
|
||||||
const struct icmp_echo_hdr *echo_reply =
|
|
||||||
(const struct icmp_echo_hdr *)(const void *)outer_icmp_bytes;
|
|
||||||
if (echo_reply->id == expected_id &&
|
|
||||||
echo_reply->seqno == expected_sequence) {
|
|
||||||
return TRACE_REPLY_DESTINATION;
|
return TRACE_REPLY_DESTINATION;
|
||||||
}
|
}
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
bool exceeded = icmp[0] == (v6 ? 3U : ICMP_TE) && icmp[1] == 0U;
|
||||||
if (ICMPH_TYPE(outer_icmp) != ICMP_TE &&
|
bool unreachable = icmp[0] == (v6 ? 1U : ICMP_DUR) && icmp[1] <= (v6 ? 7U : 15U);
|
||||||
ICMPH_TYPE(outer_icmp) != ICMP_DUR) {
|
if (!exceeded && !unreachable) {
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
const uint8_t *inner = icmp + 8;
|
||||||
/* ICMP errors quote the original IPv4 header and at least 8 payload bytes. */
|
size_t available = total - header - 8U;
|
||||||
size_t inner_offset = outer_header_length + sizeof(struct icmp_hdr);
|
size_t inner_header = v6 ? 40U : 20U;
|
||||||
if (inner_offset > available || available - inner_offset < IP_HLEN) {
|
if (available < inner_header + 8U || inner[0] >> 4 != (v6 ? 6 : 4) ||
|
||||||
|
memcmp(inner + destination_offset, destination, address_size) != 0) {
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
if (v6) {
|
||||||
const struct ip_hdr *inner_ip =
|
if (inner[6] != IPPROTO_ICMPV6 || read_be16(inner + 4) < 8U) {
|
||||||
(const struct ip_hdr *)(const void *)(packet + inner_offset);
|
|
||||||
size_t inner_header_length = IPH_HL_BYTES(inner_ip);
|
|
||||||
if (IPH_V(inner_ip) != 4U || inner_header_length < IP_HLEN ||
|
|
||||||
inner_header_length > available - inner_offset ||
|
|
||||||
available - inner_offset - inner_header_length < sizeof(struct icmp_echo_hdr) ||
|
|
||||||
IPH_PROTO(inner_ip) != IPPROTO_ICMP ||
|
|
||||||
inner_ip->dest.addr != expected_destination ||
|
|
||||||
lwip_ntohs(IPH_LEN(inner_ip)) <
|
|
||||||
inner_header_length + sizeof(struct icmp_echo_hdr)) {
|
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const struct icmp_echo_hdr *quoted_echo =
|
inner_header = (inner[0] & 15U) * 4U;
|
||||||
(const struct icmp_echo_hdr *)(const void *)(
|
if (inner_header < 20U || inner_header + 8U > available ||
|
||||||
packet + inner_offset + inner_header_length);
|
read_be16(inner + 2) < inner_header + 8U || inner[9] != IPPROTO_ICMP ||
|
||||||
if (ICMPH_TYPE((const struct icmp_hdr *)quoted_echo) != ICMP_ECHO ||
|
(read_be16(inner + 6) & 0x3fffU) != 0U ||
|
||||||
quoted_echo->id != expected_id ||
|
!checksum_valid(checksum_sum(inner, inner_header, 0))) {
|
||||||
quoted_echo->seqno != expected_sequence) {
|
|
||||||
return TRACE_REPLY_UNRELATED;
|
return TRACE_REPLY_UNRELATED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ICMPH_TYPE(outer_icmp) == ICMP_DUR) {
|
|
||||||
if (unreachable_code != NULL) {
|
|
||||||
*unreachable_code = ICMPH_CODE(outer_icmp);
|
|
||||||
}
|
}
|
||||||
|
const uint8_t *echo = inner + inner_header;
|
||||||
|
if (echo[0] != (v6 ? 128U : ICMP_ECHO) || echo[1] != 0U ||
|
||||||
|
memcmp(echo + 4, &expected_id, 2) != 0 ||
|
||||||
|
memcmp(echo + 6, &expected_sequence, 2) != 0) {
|
||||||
|
return TRACE_REPLY_UNRELATED;
|
||||||
|
}
|
||||||
|
if (unreachable) {
|
||||||
|
*unreachable_code = icmp[1];
|
||||||
return TRACE_REPLY_UNREACHABLE;
|
return TRACE_REPLY_UNREACHABLE;
|
||||||
}
|
}
|
||||||
return TRACE_REPLY_HOP;
|
return TRACE_REPLY_HOP;
|
||||||
@@ -601,7 +751,7 @@ typedef enum {
|
|||||||
} trace_wait_result_t;
|
} trace_wait_result_t;
|
||||||
|
|
||||||
static trace_wait_result_t wait_for_trace_reply(int socket_fd,
|
static trace_wait_result_t wait_for_trace_reply(int socket_fd,
|
||||||
const struct sockaddr_in *target,
|
const diagnostic_target_t *target,
|
||||||
uint16_t expected_id,
|
uint16_t expected_id,
|
||||||
uint16_t expected_sequence,
|
uint16_t expected_sequence,
|
||||||
int64_t sent_at_us,
|
int64_t sent_at_us,
|
||||||
@@ -615,7 +765,9 @@ static trace_wait_result_t wait_for_trace_reply(int socket_fd,
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int64_t remaining_us = deadline_us - esp_timer_get_time();
|
int64_t remaining_us = deadline_us - esp_timer_get_time();
|
||||||
if (remaining_us <= 0) {
|
/* IDF floors timeval to milliseconds; zero means an infinite mailbox
|
||||||
|
* wait. Stop up to 999 us early rather than installing that timeout. */
|
||||||
|
if (remaining_us < 1000) {
|
||||||
return TRACE_WAIT_TIMEOUT;
|
return TRACE_WAIT_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,7 +781,7 @@ static trace_wait_result_t wait_for_trace_reply(int socket_fd,
|
|||||||
return TRACE_WAIT_ERROR;
|
return TRACE_WAIT_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sockaddr_in source = {0};
|
struct sockaddr_storage source = {0};
|
||||||
socklen_t source_length = sizeof(source);
|
socklen_t source_length = sizeof(source);
|
||||||
ssize_t received = recvfrom(socket_fd, reply, sizeof(reply), 0,
|
ssize_t received = recvfrom(socket_fd, reply, sizeof(reply), 0,
|
||||||
(struct sockaddr *)&source, &source_length);
|
(struct sockaddr *)&source, &source_length);
|
||||||
@@ -644,15 +796,28 @@ static trace_wait_result_t wait_for_trace_reply(int socket_fd,
|
|||||||
}
|
}
|
||||||
|
|
||||||
trace_reply_kind_t kind = parse_trace_reply(
|
trace_reply_kind_t kind = parse_trace_reply(
|
||||||
reply, (size_t)received, expected_id, expected_sequence,
|
reply, (size_t)received, target, expected_id, expected_sequence, unreachable_code);
|
||||||
target->sin_addr.s_addr, unreachable_code);
|
|
||||||
if (kind == TRACE_REPLY_UNRELATED) {
|
if (kind == TRACE_REPLY_UNRELATED) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* An echo reply is the destination only when it came from our target. */
|
if (source.ss_family != target->address.ss_family || source_length < target->length) {
|
||||||
if (kind == TRACE_REPLY_DESTINATION &&
|
continue;
|
||||||
source.sin_addr.s_addr != target->sin_addr.s_addr) {
|
}
|
||||||
|
if (source.ss_family == AF_INET6) {
|
||||||
|
const struct sockaddr_in6 *from = (const struct sockaddr_in6 *)&source;
|
||||||
|
const struct sockaddr_in6 *to = (const struct sockaddr_in6 *)&target->address;
|
||||||
|
/* Bound raw sockets already enforce ingress interface. Global
|
||||||
|
* source addresses legitimately carry no zone in recvfrom. */
|
||||||
|
if ((to->sin6_scope_id != 0U && from->sin6_scope_id != 0U &&
|
||||||
|
from->sin6_scope_id != to->sin6_scope_id) ||
|
||||||
|
(kind == TRACE_REPLY_DESTINATION &&
|
||||||
|
memcmp(&from->sin6_addr, &to->sin6_addr, sizeof(to->sin6_addr)) != 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (kind == TRACE_REPLY_DESTINATION &&
|
||||||
|
((const struct sockaddr_in *)&source)->sin_addr.s_addr !=
|
||||||
|
((const struct sockaddr_in *)&target->address)->sin_addr.s_addr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,34 +851,49 @@ static void print_trace_rtt(int64_t round_trip_us)
|
|||||||
|
|
||||||
static int execute_traceroute(int argc, char **argv)
|
static int execute_traceroute(int argc, char **argv)
|
||||||
{
|
{
|
||||||
if ((argc != 2 && argc != 3) || argv[1] == NULL || *argv[1] == '\0') {
|
diagnostic_arguments_t args;
|
||||||
|
if (!parse_arguments(argc, argv, TRACEROUTE_DEFAULT_HOPS, TRACEROUTE_MAX_HOPS, &args)) {
|
||||||
print_command_usage("traceroute");
|
print_command_usage("traceroute");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
uint32_t max_hops = args.limit;
|
||||||
uint32_t max_hops = TRACEROUTE_DEFAULT_HOPS;
|
diagnostic_target_t target;
|
||||||
if (argc == 3 &&
|
|
||||||
!parse_bounded_u32(argv[2], TRACEROUTE_MIN_HOPS,
|
|
||||||
TRACEROUTE_MAX_HOPS, &max_hops)) {
|
|
||||||
print_command_usage("traceroute");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("traceroute: IPv4 only (one ICMP echo probe per hop)\n");
|
|
||||||
|
|
||||||
struct sockaddr_in target = {0};
|
|
||||||
char target_numeric[NUMERIC_ADDRESS_CAPACITY];
|
char target_numeric[NUMERIC_ADDRESS_CAPACITY];
|
||||||
if (resolve_traceroute_target(argv[1], &target,
|
if (resolve_target(&args, &target, target_numeric, sizeof(target_numeric)) != 0) {
|
||||||
target_numeric, sizeof(target_numeric)) != 0) {
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
bool v6 = target.address.ss_family == AF_INET6;
|
||||||
int socket_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
printf("traceroute: one ICMP echo probe per hop, 1 s receive deadline.\n");
|
||||||
|
if (v6) {
|
||||||
|
printf("IPv6 extension headers/fragments in replies or quotes are unsupported and ignored.\n");
|
||||||
|
}
|
||||||
|
int socket_fd = socket(target.address.ss_family, SOCK_RAW,
|
||||||
|
v6 ? IPPROTO_ICMPV6 : IPPROTO_ICMP);
|
||||||
if (socket_fd < 0) {
|
if (socket_fd < 0) {
|
||||||
printf("traceroute: could not create raw ICMP socket: %s\n", strerror(errno));
|
printf("traceroute: could not create raw ICMP socket: %s\n", strerror(errno));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (v6) {
|
||||||
|
int only_v6 = 1;
|
||||||
|
if (setsockopt(socket_fd, IPPROTO_IPV6, IPV6_V6ONLY,
|
||||||
|
&only_v6, sizeof(only_v6)) != 0) {
|
||||||
|
printf("traceroute: could not restrict socket to IPv6: %s\n", strerror(errno));
|
||||||
|
close(socket_fd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
uint32_t zone = ((const struct sockaddr_in6 *)&target.address)->sin6_scope_id;
|
||||||
|
if (zone != 0U) {
|
||||||
|
struct ifreq interface = {0};
|
||||||
|
if (if_indextoname(zone, interface.ifr_name) == NULL ||
|
||||||
|
setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE,
|
||||||
|
&interface, sizeof(interface)) != 0) {
|
||||||
|
printf("traceroute: could not bind device interface: %s\n", strerror(errno));
|
||||||
|
close(socket_fd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const struct timeval one_second = {
|
const struct timeval one_second = {
|
||||||
.tv_sec = 1,
|
.tv_sec = 1,
|
||||||
.tv_usec = 0,
|
.tv_usec = 0,
|
||||||
@@ -726,7 +906,7 @@ static int execute_traceroute(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
printf("traceroute to %s (%s), %" PRIu32 " hops max\n",
|
printf("traceroute to %s (%s), %" PRIu32 " hops max\n",
|
||||||
argv[1], target_numeric, max_hops);
|
args.host, target_numeric, max_hops);
|
||||||
|
|
||||||
/* A per-run ID plus one sequence per hop rejects other raw-socket traffic. */
|
/* A per-run ID plus one sequence per hop rejects other raw-socket traffic. */
|
||||||
uint16_t identifier = lwip_htons((uint16_t)esp_timer_get_time());
|
uint16_t identifier = lwip_htons((uint16_t)esp_timer_get_time());
|
||||||
@@ -735,6 +915,8 @@ static int execute_traceroute(int argc, char **argv)
|
|||||||
uint32_t final_hop = 0U;
|
uint32_t final_hop = 0U;
|
||||||
|
|
||||||
for (uint32_t hop = 1U; hop <= max_hops; ++hop) {
|
for (uint32_t hop = 1U; hop <= max_hops; ++hop) {
|
||||||
|
/* IDF 5.5 lwIP uses the common PCB ttl for IPv6 hop limit too;
|
||||||
|
* IPV6_UNICAST_HOPS is not implemented. ICMPv6 TX checksum is automatic. */
|
||||||
int ttl = (int)hop;
|
int ttl = (int)hop;
|
||||||
if (setsockopt(socket_fd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) != 0) {
|
if (setsockopt(socket_fd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) != 0) {
|
||||||
printf("traceroute: could not set TTL for hop %" PRIu32 ": %s\n",
|
printf("traceroute: could not set TTL for hop %" PRIu32 ": %s\n",
|
||||||
@@ -744,17 +926,19 @@ static int execute_traceroute(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct icmp_echo_hdr probe = {
|
struct icmp_echo_hdr probe = {
|
||||||
.type = ICMP_ECHO,
|
.type = v6 ? 128U : ICMP_ECHO,
|
||||||
.code = 0U,
|
.code = 0U,
|
||||||
.chksum = 0U,
|
.chksum = 0U,
|
||||||
.id = identifier,
|
.id = identifier,
|
||||||
.seqno = lwip_htons((uint16_t)hop),
|
.seqno = lwip_htons((uint16_t)hop),
|
||||||
};
|
};
|
||||||
|
if (!v6) {
|
||||||
probe.chksum = inet_chksum(&probe, (u16_t)sizeof(probe));
|
probe.chksum = inet_chksum(&probe, (u16_t)sizeof(probe));
|
||||||
|
}
|
||||||
|
|
||||||
int64_t sent_at_us = esp_timer_get_time();
|
int64_t sent_at_us = esp_timer_get_time();
|
||||||
ssize_t sent = sendto(socket_fd, &probe, sizeof(probe), 0,
|
ssize_t sent = sendto(socket_fd, &probe, sizeof(probe), 0,
|
||||||
(const struct sockaddr *)&target, sizeof(target));
|
(const struct sockaddr *)&target.address, target.length);
|
||||||
if (sent != (ssize_t)sizeof(probe)) {
|
if (sent != (ssize_t)sizeof(probe)) {
|
||||||
printf("traceroute: probe send failed at hop %" PRIu32 ": %s\n",
|
printf("traceroute: probe send failed at hop %" PRIu32 ": %s\n",
|
||||||
hop, strerror(errno));
|
hop, strerror(errno));
|
||||||
@@ -844,21 +1028,21 @@ esp_err_t network_console_register_root_commands(void)
|
|||||||
static const esp_console_cmd_t commands[] = {
|
static const esp_console_cmd_t commands[] = {
|
||||||
{
|
{
|
||||||
.command = "ping",
|
.command = "ping",
|
||||||
.help = "ping <host> [count] (count 1..20, default 4)",
|
.help = "ping [-4|-6] <host> [count] (count 1..20, default 4)",
|
||||||
.hint = NULL,
|
.hint = NULL,
|
||||||
.func = &network_console_execute,
|
.func = &network_console_execute,
|
||||||
.argtable = NULL,
|
.argtable = NULL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.command = "nslookup",
|
.command = "nslookup",
|
||||||
.help = "nslookup <host> (print unique numeric IPv4/IPv6 addresses)",
|
.help = "nslookup [-4|-6] <host> (default: query A and AAAA separately)",
|
||||||
.hint = NULL,
|
.hint = NULL,
|
||||||
.func = &network_console_execute,
|
.func = &network_console_execute,
|
||||||
.argtable = NULL,
|
.argtable = NULL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.command = "traceroute",
|
.command = "traceroute",
|
||||||
.help = "traceroute <host> [max-hops] (IPv4 only; 1..30, default 16)",
|
.help = "traceroute [-4|-6] <host> [max-hops] (1..30, default 16)",
|
||||||
.hint = NULL,
|
.hint = NULL,
|
||||||
.func = &network_console_execute,
|
.func = &network_console_execute,
|
||||||
.argtable = NULL,
|
.argtable = NULL,
|
||||||
|
|||||||
+10
-1
@@ -16,7 +16,16 @@ esp_err_t network_console_register_root_commands(void);
|
|||||||
/* Return true only for a root command handled by network_console_execute(). */
|
/* Return true only for a root command handled by network_console_execute(). */
|
||||||
bool network_console_is_command(const char *name);
|
bool network_console_is_command(const char *name);
|
||||||
|
|
||||||
/* Execute ping, nslookup, or traceroute; argv[0] selects the operation. */
|
/* Canonical handler for every frontend and wifi alias; argv[0] selects:
|
||||||
|
* 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 also follow the host/count. Probe hostnames default to
|
||||||
|
* IPv4, then IPv6 only if resolution returns no address; nslookup queries both.
|
||||||
|
* IPv6 link-local probes require a device-local %sta/%ap/index/lwIP-name zone;
|
||||||
|
* nslookup displays unscoped link-local records/literals without probing them.
|
||||||
|
* Call only through the serialized administration dispatcher.
|
||||||
|
*/
|
||||||
int network_console_execute(int argc, char **argv);
|
int network_console_execute(int argc, char **argv);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
+4
-3
@@ -34,9 +34,10 @@ static void print_usage(void)
|
|||||||
printf(" wifi ap channel <1..11>\n");
|
printf(" wifi ap channel <1..11>\n");
|
||||||
printf(" wifi ap secret|show-secret\n");
|
printf(" wifi ap secret|show-secret\n");
|
||||||
printf(" wifi save|load|defaults|reset\n");
|
printf(" wifi save|load|defaults|reset\n");
|
||||||
printf(" wifi ping <host> [count]\n");
|
printf(" wifi ping [-4|-6] <host> [count] (1..20, default 4)\n");
|
||||||
printf(" wifi nslookup <host>\n");
|
printf(" wifi nslookup [-4|-6] <host> (default: A and AAAA)\n");
|
||||||
printf(" wifi traceroute <host> [max-hops]\n");
|
printf(" wifi traceroute [-4|-6] <host> [max-hops] (1..30, default 16)\n");
|
||||||
|
printf(" Diagnostic family flags may also follow the host or count/max-hops.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool parse_u32(const char *text, uint32_t maximum, uint32_t *value)
|
static bool parse_u32(const char *text, uint32_t maximum, uint32_t *value)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 *);
|
||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
@@ -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:≈ }
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user