Add Dual-Stack Network Diagnostics
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# Shared IPv4/IPv6 diagnostics fixtures and integration handoff
|
||||
|
||||
## Commands
|
||||
|
||||
```text
|
||||
ping [-4|-6] <host> [count] count 1..20, default 4
|
||||
traceroute [-4|-6] <host> [max-hops] hops 1..30, default 16
|
||||
nslookup [-4|-6] <host>
|
||||
```
|
||||
|
||||
One family flag may occur anywhere after the command, including after the host or
|
||||
numeric argument. Repeated/conflicting flags, unknown options, empty arguments,
|
||||
extra arguments, and non-decimal/out-of-range counts are rejected. Existing bare
|
||||
commands and aliases calling `network_console_execute()` share these semantics.
|
||||
For example: `ping fe80::1%sta -6 3`, `traceroute example.net 20 -6`.
|
||||
|
||||
Default hostname probes resolve IPv4 first, then IPv6 only when resolution returns
|
||||
no address (`EAI_FAIL`/`EAI_NONAME`). They **never** retry another family after a
|
||||
send failure, timeout, or unreachable reply. This is not Happy Eyeballs. Explicit
|
||||
`-6` neither requests nor accepts IPv4/mapped IPv4. Default hostname `nslookup`
|
||||
makes separate A and AAAA queries, prints partial success, and explains resolver
|
||||
limits. Numeric literals do not query DNS or perform reverse lookup.
|
||||
|
||||
Zones refer to interfaces **on the ESP32**, not on the browser/SSH client:
|
||||
`%sta`, `%ap`, a currently valid numeric lwIP index (1..255), or an existing lwIP
|
||||
interface name. Ping/traceroute reject unscoped link-local addresses (including
|
||||
DNS AAAA answers with zone zero), showing the address and scoped-literal examples;
|
||||
no default interface is guessed. `nslookup` displays those records and unscoped
|
||||
link-local literals with a note that probes require an explicit device scope.
|
||||
Mapped IPv4 literals remain rejected. Scoped ping preserves `sin6_scope_id` as the `ip6_addr_t` zone **and** sets
|
||||
`esp_ping_config_t.interface`; scoped traceroute retains the socket zone and binds
|
||||
the raw socket to that interface. Numeric output includes the zone.
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
python3 tests/network_diagnostics/run.py
|
||||
python3 tests/network_diagnostics/run.py --sanitize # optional installed ASan/UBSan
|
||||
python3 tests/network_diagnostics/sdk_contract.py /path/to/framework-espidf
|
||||
```
|
||||
|
||||
`run.py` includes the actual production `src/network_console.c`, not a duplicate
|
||||
parser/implementation. SDK calls and socket operations are deterministic fakes;
|
||||
there is no networking or hardware access. It checks argument permutations and
|
||||
bounds, per-family resolution/fallback/freeing, A+AAAA queries, scope parsing and
|
||||
ping zone/interface conversion, valid echo/time-exceeded/unreachable packets,
|
||||
all truncation lengths, bad checksums/IDs/sequences/targets/codes/protocols,
|
||||
unsupported IPv6 extension headers, recv source correlation, absolute deadlines,
|
||||
1/500/999/1000-us remaining-budget boundaries and unrelated traffic leaving 500 us,
|
||||
zero-zone link-local AAAA display versus rejection before any probe socket/session,
|
||||
EINTR, all configured socket-option failure points, send/receive/create errors,
|
||||
fd-zero cleanup, PSRAM allocation failure, and ping create/start/delayed-completion
|
||||
lifetime handling. Also runs 10,000 deterministic malformed packet inputs.
|
||||
|
||||
## Audited IDF 5.5.0 / lwIP contract
|
||||
|
||||
`sdk_contract.py` pins seven installed source files, including the FreeRTOS mailbox
|
||||
port. It also extracts and compiles the actual SDK timeval conversion macro and
|
||||
`sys_arch_mbox_fetch()` with a fake queue boundary, checking the infinite-wait
|
||||
sentinel for sub-millisecond values and finite waits for >=1 ms at 1/10-ms ticks.
|
||||
The production module also has an IDF version guard; upgrades require re-audit.
|
||||
|
||||
- `api/netdb.c`: `AF_INET` and `AF_INET6` choose explicit DNS address types. Current
|
||||
generated `CONFIG_LWIP_DNS_MAX_HOST_IP=1` means one returned address per query;
|
||||
this command prints one selected address per family, not a complete RRset.
|
||||
DNS errors collapse into `EAI_FAIL`; NXDOMAIN, timeout, and server failure cannot
|
||||
reliably be distinguished. Resolution itself uses SDK DNS timeouts, not a
|
||||
command-owned deadline. `.local` depends on lwIP mDNS-query support, not client NSS.
|
||||
- `core/ipv6/ip6.c` restores the complete IPv6 header before `raw_input()`;
|
||||
`api/api_msg.c:recv_raw()` copies it. Raw receives are **not Linux's ICMPv6-only
|
||||
framing**. Raw delivery precedes ICMP checksum validation, so traceroute checks
|
||||
the complete outer ICMP checksum (including IPv6 pseudoheader) itself.
|
||||
- `core/raw.c` demultiplexes the IPv6 **base** next-header. Therefore traceroute
|
||||
explicitly rejects all outer/quoted IPv6 extension headers, including fragment,
|
||||
routing, hop-by-hop, destination options, AH and ESP. It does not walk or guess
|
||||
unsupported forms. Quoted packets may be incomplete after the required echo
|
||||
header, but the complete outer packet must fit/be received and pass its checksum.
|
||||
- `api/api_msg.c` enables checksum generation at offset 2 for raw ICMPv6 sockets.
|
||||
`api/sockets.c` refuses changing `IPV6_CHECKSUM` for ICMPv6; do not set it.
|
||||
`IPPROTO_IP/IP_TTL` sets the common PCB TTL used for **both** IPv4 TTL and IPv6
|
||||
hop limit. `IPV6_UNICAST_HOPS` is not implemented. Fixtures require the actual
|
||||
supported option for both families. IPv6 traceroute also sets `IPV6_V6ONLY`.
|
||||
SDK ping's `IPPROTO_IP/IP_TTL` use is likewise correct for IPv6; it is unchanged.
|
||||
- `api/sockets.c` floors `SO_RCVTIMEO` timeval values to milliseconds;
|
||||
`port/freertos/sys_arch.c:sys_arch_mbox_fetch()` interprets zero milliseconds
|
||||
as `portMAX_DELAY`, not a poll. Traceroute stops when <1000 us remain, never
|
||||
supplying a zero-millisecond receive timeout.
|
||||
- `apps/ping/ping_sock.c` does not copy the target zone into `sin6_scope_id`;
|
||||
`config.interface` uses `SO_BINDTODEVICE` and is required for scoped ping.
|
||||
SDK ping is not the strict traceroute parser: its receive validation and IPv6
|
||||
profile formatting remain upstream behavior (reply profiles do not retain the
|
||||
interface zone). Do not infer traceroute's packet correlation guarantees for
|
||||
SDK ping.
|
||||
|
||||
## Bounds and remaining SDK limitation
|
||||
|
||||
Traceroute owns one raw socket in the dispatcher, sends one eight-byte probe per
|
||||
hop, and has a one-second **absolute receive deadline** per hop (unrelated packets
|
||||
and EINTR cannot extend it). It may stop up to 999 us early to avoid lwIP's
|
||||
zero-millisecond infinite-wait sentinel. Every post-creation exit closes it. The 1280-byte
|
||||
receive buffer rejects larger/truncated outer replies rather than partially
|
||||
matching them. DNS, scheduler and synchronous send latency are not included in
|
||||
that deadline. No extra task, serial hot-path work, or persistent trace allocation.
|
||||
|
||||
Ping keeps the existing 21-event, **4200-byte PSRAM-only**, lazily retained queue
|
||||
payload and SDK's transient task. Count <=20, timeout=1000ms, interval=1000ms.
|
||||
The console wait has an absolute `count * 2000 + 2000` ms budget after start.
|
||||
Callback context is now permanent (not a caller-stack pointer); if that deadline
|
||||
expires, the callback queue remains reserved until its END event. Until then,
|
||||
later ping calls fail busy rather than overlapping callback-producing sessions or
|
||||
resetting their queue. **END ends callback production, not SDK task/socket
|
||||
retirement:** `esp_ping_delete_session()` only requests asynchronous deletion.
|
||||
A new command may start while the previous task/socket is briefly retiring; this
|
||||
is not a guarantee that two SDK tasks/sockets can never coexist.
|
||||
|
||||
**Hard ping socket-lifetime bounds are not available from this SDK API:** unrelated
|
||||
raw ICMP traffic can repeatedly refresh `esp_ping_receive()`'s receive timeout;
|
||||
`stop`/`delete` are asynchronous and cannot interrupt that loop. The timeout path
|
||||
must not touch a potentially concurrently deleted handle. The retained reservation
|
||||
is intentional failure isolation, not proof of synchronous cleanup. A strict hard
|
||||
socket deadline would require replacing SDK ping with a dispatcher-owned raw ping
|
||||
implementation or an SDK cancellation helper with an acknowledged lifetime
|
||||
contract. No dependency/SDK patch was made. This limitation is for the parent to
|
||||
assess; ordinary SDK completion still self-deletes and releases its transient task.
|
||||
|
||||
## Validation and measured impact (2026-09-21)
|
||||
|
||||
- Host production fixtures: **8027 checks + 10,000 fuzz inputs PASS**.
|
||||
- Installed seven-file SDK hash contract and actual timeout conversion/mailbox fixtures: PASS.
|
||||
- Actual target compiler: syntax-only PASS after the review fixes. Earlier isolated
|
||||
baseline/current object compiles used the existing `network_console.c` compilation
|
||||
arguments. No `pio` build, project build artifact writes, upload, monitor, or
|
||||
hardware test.
|
||||
- Pre-review object measurement (not remeasured after the timeout/scope fixes):
|
||||
`text`: 7281 -> 9917 bytes (**+2636**); `bss`: 92 -> 97 (**+5**);
|
||||
`data`: unchanged 0. These are object measurements, **not final linked firmware**.
|
||||
- Pre-review target `-fstack-usage` frames: `execute_ping` 400 -> 464; `execute_nslookup`
|
||||
128 -> 144; `execute_traceroute` 224 -> 256; `wait_for_trace_reply` 240 -> 1408.
|
||||
These individual frames are not a measured runtime stack high-water mark.
|
||||
- ASan/UBSan attempt could not link: host `libasan.so.8.0.0` and
|
||||
`libubsan.so.1.0.0` are missing. Ordinary `-Wall -Wextra -Werror` fixtures passed.
|
||||
|
||||
## Integrated surface and build validation
|
||||
|
||||
Wi-Fi help and root/alias completion use the syntax above. The production console
|
||||
boundary is covered by `python3 tests/network_diagnostics_surfaces/run.py`:
|
||||
432 SSH/browser root-and-alias cases, UART0 parity, role/revocation guards, and
|
||||
fixed-token completion. All surfaces forward to the same canonical handler.
|
||||
|
||||
No new ESP-NETIF ownership helper is needed: public
|
||||
`esp_netif_get_handle_from_ifkey()` / `esp_netif_get_netif_impl_index()` map aliases.
|
||||
IPv4+IPv6, raw sockets and IPv6 scopes must remain enabled. The installed
|
||||
configuration supplies these; no configuration/dependency changes were made.
|
||||
|
||||
Final `pio run` passed: **94,452 B linked RAM / 1,857,673 B flash**
|
||||
(+8 B / +3,188 B versus the recorded pre-follow-up build). This is not runtime
|
||||
heap or stack headroom. Host fixtures, exact SDK contract, surface tests and
|
||||
existing admin policy/Wi-Fi prompt regressions passed. No upload, packet capture,
|
||||
live DNS, multi-hop trace or hardware validation was performed. The prior Phase12
|
||||
acceptance must not be treated as acceptance of this later diagnostics change.
|
||||
@@ -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