Add Dual-Stack Network Diagnostics
This commit is contained in:
@@ -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