Public Access
Shell: ping, nslookup, port, traceroute, ifconfig and arp (#90)
Network troubleshooting from the device itself, in the Shell and over both consoles. ping, nslookup, port and traceroute each run on a task of their own and print as they go, to the console that asked; one at a time, and `cancel` stops it. ifconfig and arp answer at once: the interfaces (Wi-Fi and the VPN), which is the default route, the DNS servers, the neighbours. nslookup asks a DNS server itself, so it can say which server answered and in how long, and ask another. A sized ping finds what a tunnel really carries. With a how-to, "When the network doesn't work", and the rest of the docs. tls, ntp and netstat from the issue's list are not in this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include "apps/demo_app.h"
|
||||
#include "apps/note_editor.h"
|
||||
#include "apps/shell_app.h"
|
||||
#include "services/net_tools.h"
|
||||
#include "services/vpn_service.h"
|
||||
#include "services/web_share.h"
|
||||
#include "apps/gemini_app.h"
|
||||
@@ -89,6 +90,7 @@ static IrcService* irc;
|
||||
static UpdateService* update;
|
||||
static DebugConsole* debugConsole;
|
||||
static VpnService* vpnService; // not in Safe Mode
|
||||
static NetTools netTools; // ping, nslookup and the rest (issue #90)
|
||||
static Notifier* notifier;
|
||||
static LauncherApp launcher;
|
||||
static AppManager* apps;
|
||||
@@ -674,6 +676,8 @@ static const char* const kHelp =
|
||||
"update check | update list | update status | update install <tag> the project's releases on Gitea\n"
|
||||
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||
"Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command\n"
|
||||
"ping <host> [count] [size] | nslookup <name> [server] | port <host> <port> | traceroute <host> | cancel is it there, does its name resolve, is its port open, which way; one at a time\n"
|
||||
"ifconfig | arp the interfaces (Wi-Fi and the VPN), their addresses, the default route and the DNS servers; the neighbours heard\n"
|
||||
"vpn status | vpn up [seconds] | vpn down | vpn import [path] | vpn forget | vpn auto on|off the WireGuard tunnel (Settings > VPN); import reads /vpn/wg0.conf; with seconds, it goes down by itself\n"
|
||||
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
|
||||
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
|
||||
@@ -866,6 +870,8 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
else console.println("Not now: Setup is running");
|
||||
return;
|
||||
}
|
||||
if (netTools.command(line.c_str())) return;
|
||||
if (line == "cancel") netTools.cancel(); // and a copy or a delete, below
|
||||
if (line == "vpn" || line.startsWith("vpn ")) return vpnCommand(line.length() > 4 ? line.substring(4) : String("status"));
|
||||
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "services/net_tools.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <lwip/etharp.h>
|
||||
#include <lwip/dns.h>
|
||||
#include <lwip/netdb.h>
|
||||
#include <lwip/netif.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include <lwip/tcpip.h>
|
||||
|
||||
#include <esp_random.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "ipv4.h"
|
||||
#include "net_probe.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
constexpr int kMaxHops = 20;
|
||||
constexpr uint32_t kPingEveryMs = 1000, kPingWaitMs = 1000, kHopWaitMs = 2000, kPortWaitMs = 5000, kDnsWaitMs = 3000;
|
||||
|
||||
struct LwipLock {
|
||||
LwipLock() { LOCK_TCPIP_CORE(); }
|
||||
~LwipLock() { UNLOCK_TCPIP_CORE(); }
|
||||
};
|
||||
|
||||
std::string text(uint32_t networkOrder) { return net::formatIpv4(lwip_ntohl(networkOrder)); }
|
||||
|
||||
// A name or an address, as an address in network order. False: it doesn't resolve.
|
||||
bool resolve(const std::string& host, uint32_t& out) {
|
||||
uint32_t ip;
|
||||
if (net::parseIpv4(host, ip)) {
|
||||
out = lwip_htonl(ip);
|
||||
return true;
|
||||
}
|
||||
struct addrinfo hints = {};
|
||||
hints.ai_family = AF_INET;
|
||||
struct addrinfo* found = nullptr;
|
||||
if (lwip_getaddrinfo(host.c_str(), nullptr, &hints, &found) != 0 || !found) return false;
|
||||
out = reinterpret_cast<struct sockaddr_in*>(found->ai_addr)->sin_addr.s_addr;
|
||||
lwip_freeaddrinfo(found);
|
||||
return true;
|
||||
}
|
||||
|
||||
void waitMs(int socket, uint32_t ms) {
|
||||
struct timeval tv = {static_cast<time_t>(ms / 1000), static_cast<suseconds_t>((ms % 1000) * 1000)};
|
||||
lwip_setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct NetTools::Job {
|
||||
enum class Kind { Ping, Trace, Port, Lookup } kind;
|
||||
NetTools* owner;
|
||||
Console::Origin from;
|
||||
net::PingArgs ping;
|
||||
net::PortArgs port;
|
||||
net::LookupArgs lookup;
|
||||
|
||||
bool stopped() const { return owner->stop_; }
|
||||
// Sends one echo request and waits for what answers it. The time in ms, or -1; `from` and
|
||||
// `kind` say who answered and how.
|
||||
int echo(int socket, uint32_t to, uint16_t id, uint16_t seq, int size, uint32_t waitFor, uint32_t& from, net::IcmpAnswer::Kind& kind);
|
||||
void runPing();
|
||||
void runTrace();
|
||||
void runPort();
|
||||
void runLookup();
|
||||
};
|
||||
|
||||
int NetTools::Job::echo(int socket, uint32_t to, uint16_t id, uint16_t seq, int size, uint32_t waitFor, uint32_t& from, net::IcmpAnswer::Kind& kind) {
|
||||
uint8_t packet[8 + 1400], answer[128];
|
||||
size_t len = net::buildEcho(packet, sizeof packet, id, seq, static_cast<size_t>(size));
|
||||
struct sockaddr_in dest = {};
|
||||
dest.sin_family = AF_INET;
|
||||
dest.sin_addr.s_addr = to;
|
||||
uint32_t sent = micros();
|
||||
if (lwip_sendto(socket, packet, len, 0, reinterpret_cast<struct sockaddr*>(&dest), sizeof dest) < 0) return -2;
|
||||
while (!stopped()) {
|
||||
uint32_t gone = (micros() - sent) / 1000;
|
||||
if (gone >= waitFor) break;
|
||||
waitMs(socket, std::min<uint32_t>(waitFor - gone, 200)); // short waits: `cancel` is noticed
|
||||
struct sockaddr_in who = {};
|
||||
socklen_t wholen = sizeof who;
|
||||
int n = lwip_recvfrom(socket, answer, sizeof answer, 0, reinterpret_cast<struct sockaddr*>(&who), &wholen);
|
||||
if (n <= 0) continue;
|
||||
net::IcmpAnswer a = net::parseIcmp(answer, static_cast<size_t>(n));
|
||||
if (a.kind == net::IcmpAnswer::Kind::Other || a.id != id || a.seq != seq) continue; // somebody else's
|
||||
from = who.sin_addr.s_addr;
|
||||
kind = a.kind;
|
||||
return static_cast<int>((micros() - sent + 500) / 1000);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void NetTools::Job::runPing() {
|
||||
uint32_t to;
|
||||
if (!resolve(ping.host, to)) return (void)console.printf("ping: %s doesn't resolve\n", ping.host.c_str());
|
||||
int s = lwip_socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
||||
if (s < 0) return (void)console.println("ping: error no socket");
|
||||
console.printf("ping: %s, %d bytes\n", text(to).c_str(), ping.size);
|
||||
uint16_t id = static_cast<uint16_t>(esp_random());
|
||||
net::PingStats stats;
|
||||
for (int seq = 1; seq <= ping.count && !stopped(); seq++) {
|
||||
uint32_t started = millis(), from = 0;
|
||||
net::IcmpAnswer::Kind kind = net::IcmpAnswer::Kind::Other;
|
||||
stats.sent++;
|
||||
int ms = echo(s, to, id, static_cast<uint16_t>(seq), ping.size, kPingWaitMs, from, kind);
|
||||
if (ms == -2) console.printf("ping: %d not sent: no route, or too big\n", seq);
|
||||
else if (ms < 0) console.printf("ping: %d no answer\n", seq);
|
||||
else if (kind == net::IcmpAnswer::Kind::Echo) {
|
||||
stats.add(static_cast<uint32_t>(ms));
|
||||
console.printf("ping: %d %d ms\n", seq, ms);
|
||||
} else {
|
||||
console.printf("ping: %d %s says %s\n", seq, text(from).c_str(), kind == net::IcmpAnswer::Kind::TimeExceeded ? "too many hops" : "unreachable");
|
||||
}
|
||||
while (seq < ping.count && !stopped() && millis() - started < kPingEveryMs) delay(50);
|
||||
}
|
||||
lwip_close(s);
|
||||
console.printf("ping: %s%s\n", stopped() ? "stopped, " : "", stats.summary().c_str());
|
||||
}
|
||||
|
||||
// An echo request allowed one hop, then two, then three: each router that drops it says so, and
|
||||
// that is the list.
|
||||
void NetTools::Job::runTrace() {
|
||||
uint32_t to;
|
||||
if (!resolve(ping.host, to)) return (void)console.printf("traceroute: %s doesn't resolve\n", ping.host.c_str());
|
||||
int s = lwip_socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
||||
if (s < 0) return (void)console.println("traceroute: error no socket");
|
||||
console.printf("traceroute: to %s, %d hops at most\n", text(to).c_str(), kMaxHops);
|
||||
uint16_t id = static_cast<uint16_t>(esp_random());
|
||||
bool arrived = false;
|
||||
for (int hop = 1; hop <= kMaxHops && !stopped() && !arrived; hop++) {
|
||||
int ttl = hop;
|
||||
lwip_setsockopt(s, IPPROTO_IP, IP_TTL, &ttl, sizeof ttl);
|
||||
uint32_t from = 0;
|
||||
net::IcmpAnswer::Kind kind = net::IcmpAnswer::Kind::Other;
|
||||
int ms = echo(s, to, id, static_cast<uint16_t>(hop), 32, kHopWaitMs, from, kind);
|
||||
if (ms < 0) console.printf("traceroute: %2d *\n", hop);
|
||||
else console.printf("traceroute: %2d %s %d ms%s\n", hop, text(from).c_str(), ms, kind == net::IcmpAnswer::Kind::Unreachable ? " unreachable" : "");
|
||||
arrived = ms >= 0 && kind != net::IcmpAnswer::Kind::TimeExceeded;
|
||||
}
|
||||
lwip_close(s);
|
||||
console.printf("traceroute: %s\n", stopped() ? "stopped" : arrived ? "arrived" : "not reached");
|
||||
}
|
||||
|
||||
void NetTools::Job::runPort() {
|
||||
uint32_t to;
|
||||
if (!resolve(port.host, to)) return (void)console.printf("port: %s doesn't resolve\n", port.host.c_str());
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return (void)console.println("port: error no socket");
|
||||
lwip_fcntl(s, F_SETFL, lwip_fcntl(s, F_GETFL, 0) | O_NONBLOCK);
|
||||
struct sockaddr_in dest = {};
|
||||
dest.sin_family = AF_INET;
|
||||
dest.sin_port = lwip_htons(port.port);
|
||||
dest.sin_addr.s_addr = to;
|
||||
uint32_t started = millis();
|
||||
int error = lwip_connect(s, reinterpret_cast<struct sockaddr*>(&dest), sizeof dest) == 0 ? 0 : errno;
|
||||
bool answered = error == 0;
|
||||
while (error == EINPROGRESS && !answered && !stopped() && millis() - started < kPortWaitMs) {
|
||||
fd_set writable, failed;
|
||||
FD_ZERO(&writable);
|
||||
FD_ZERO(&failed);
|
||||
FD_SET(s, &writable);
|
||||
FD_SET(s, &failed);
|
||||
struct timeval tv = {0, 200000};
|
||||
if (lwip_select(s + 1, nullptr, &writable, &failed, &tv) > 0) {
|
||||
socklen_t len = sizeof error;
|
||||
lwip_getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len);
|
||||
answered = true;
|
||||
}
|
||||
}
|
||||
uint32_t ms = millis() - started;
|
||||
lwip_close(s);
|
||||
std::string where = text(to) + ":" + std::to_string(port.port);
|
||||
if (answered && error == 0) console.printf("port: %s open, %lu ms\n", where.c_str(), (unsigned long)ms);
|
||||
else if (answered && (error == ECONNREFUSED || error == ECONNRESET)) console.printf("port: %s refused, %lu ms: the host is there, nothing listens\n", where.c_str(), (unsigned long)ms);
|
||||
else if (answered || error != EINPROGRESS) console.printf("port: %s no route to it (error %d)\n", where.c_str(), error);
|
||||
else if (stopped()) console.println("port: stopped");
|
||||
else console.printf("port: %s no answer in %lu s: down, or filtered\n", where.c_str(), (unsigned long)(kPortWaitMs / 1000));
|
||||
}
|
||||
|
||||
// Asks one server directly, so that the answer says which server and how long, which the
|
||||
// system's own resolver doesn't.
|
||||
void NetTools::Job::runLookup() {
|
||||
uint32_t server = 0;
|
||||
if (!lookup.server.empty()) resolve(lookup.server, server);
|
||||
else {
|
||||
LwipLock lock;
|
||||
const ip_addr_t* first = dns_getserver(0);
|
||||
if (first && IP_IS_V4(first)) server = ip_2_ip4(first)->addr;
|
||||
}
|
||||
if (!server) return (void)console.println("nslookup: no DNS server is set");
|
||||
int s = lwip_socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s < 0) return (void)console.println("nslookup: error no socket");
|
||||
uint8_t query[300], answer[512];
|
||||
uint16_t id = static_cast<uint16_t>(esp_random());
|
||||
size_t len = net::buildDnsQuery(query, sizeof query, id, lookup.name);
|
||||
struct sockaddr_in dest = {};
|
||||
dest.sin_family = AF_INET;
|
||||
dest.sin_port = lwip_htons(53);
|
||||
dest.sin_addr.s_addr = server;
|
||||
uint32_t started = millis();
|
||||
net::DnsAnswer result;
|
||||
bool got = false;
|
||||
if (len && lwip_sendto(s, query, len, 0, reinterpret_cast<struct sockaddr*>(&dest), sizeof dest) >= 0) {
|
||||
while (!got && !stopped() && millis() - started < kDnsWaitMs) {
|
||||
waitMs(s, 200);
|
||||
int n = lwip_recv(s, answer, sizeof answer, 0);
|
||||
if (n > 0) got = net::parseDnsAnswer(answer, static_cast<size_t>(n), id, result);
|
||||
}
|
||||
}
|
||||
uint32_t ms = millis() - started;
|
||||
lwip_close(s);
|
||||
if (!got) return (void)console.printf("nslookup: no answer from %s in %lu s\n", text(server).c_str(), (unsigned long)(kDnsWaitMs / 1000));
|
||||
console.printf("nslookup: %s answered in %lu ms\n", text(server).c_str(), (unsigned long)ms);
|
||||
if (!result.alias.empty()) console.printf("nslookup: %s is %s\n", lookup.name.c_str(), result.alias.c_str());
|
||||
for (uint32_t ip : result.addresses) console.printf("nslookup: %s\n", net::formatIpv4(ip).c_str());
|
||||
if (result.rcode == 3) console.printf("nslookup: there is no %s\n", lookup.name.c_str());
|
||||
else if (result.rcode) console.printf("nslookup: the server refused (code %d)\n", result.rcode);
|
||||
else if (result.addresses.empty()) console.printf("nslookup: %s has no IPv4 address%s\n", lookup.name.c_str(), result.truncated ? " in a first packet" : "");
|
||||
}
|
||||
|
||||
void NetTools::task(void* arg) {
|
||||
Job* job = static_cast<Job*>(arg);
|
||||
{
|
||||
Console::As as(job->from); // the lines go to the console that asked (the Shell shows only its own)
|
||||
switch (job->kind) {
|
||||
case Job::Kind::Ping: job->runPing(); break;
|
||||
case Job::Kind::Trace: job->runTrace(); break;
|
||||
case Job::Kind::Port: job->runPort(); break;
|
||||
case Job::Kind::Lookup: job->runLookup(); break;
|
||||
}
|
||||
}
|
||||
NetTools* owner = job->owner;
|
||||
delete job;
|
||||
owner->busy_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void NetTools::start(Job* job) {
|
||||
job->owner = this;
|
||||
job->from = console.origin();
|
||||
stop_ = false;
|
||||
busy_ = true;
|
||||
if (xTaskCreate(task, "nettool", 6144, job, 1, nullptr) != pdPASS) {
|
||||
busy_ = false;
|
||||
delete job;
|
||||
console.println("net: error not enough memory for it");
|
||||
}
|
||||
}
|
||||
|
||||
bool NetTools::command(const std::string& line) {
|
||||
size_t space = line.find(' ');
|
||||
std::string name = line.substr(0, space), args = space == std::string::npos ? "" : line.substr(space + 1);
|
||||
if (name == "ifconfig") {
|
||||
LwipLock lock;
|
||||
for (struct netif* n = netif_list; n; n = n->next) {
|
||||
if (n->name[0] == 'l' && n->name[1] == 'o') continue;
|
||||
const char* label = n->name[0] == 's' && n->name[1] == 't' ? "wifi" : n->name[0] == 'w' && n->name[1] == 'g' ? "vpn" : nullptr;
|
||||
char raw[4] = {n->name[0], n->name[1], static_cast<char>('0' + n->num % 10), 0};
|
||||
bool up = netif_is_up(n) && netif_is_link_up(n);
|
||||
console.printf("ifconfig: %s %s/%d", label ? label : raw, text(netif_ip4_addr(n)->addr).c_str(), __builtin_popcount(netif_ip4_netmask(n)->addr));
|
||||
if (netif_ip4_gw(n)->addr) console.printf(" gw %s", text(netif_ip4_gw(n)->addr).c_str());
|
||||
console.printf(" mtu %u, %s%s\n", (unsigned)n->mtu, up ? "up" : "down", n == netif_default ? ", default route" : "");
|
||||
}
|
||||
std::string servers;
|
||||
for (u8_t i = 0; i < DNS_MAX_SERVERS; i++) {
|
||||
const ip_addr_t* d = dns_getserver(i);
|
||||
if (d && IP_IS_V4(d) && ip_2_ip4(d)->addr) servers += " " + text(ip_2_ip4(d)->addr);
|
||||
}
|
||||
console.printf("ifconfig: dns%s\n", servers.empty() ? " none" : servers.c_str());
|
||||
return true;
|
||||
}
|
||||
if (name == "arp") {
|
||||
LwipLock lock;
|
||||
int found = 0;
|
||||
for (size_t i = 0; i < ARP_TABLE_SIZE; i++) {
|
||||
ip4_addr_t* ip;
|
||||
struct netif* nif;
|
||||
struct eth_addr* mac;
|
||||
if (!etharp_get_entry(i, &ip, &nif, &mac)) continue;
|
||||
found++;
|
||||
console.printf("arp: %-15s %02x:%02x:%02x:%02x:%02x:%02x\n", text(ip->addr).c_str(), mac->addr[0], mac->addr[1], mac->addr[2], mac->addr[3], mac->addr[4],
|
||||
mac->addr[5]);
|
||||
}
|
||||
if (!found) console.println("arp: nobody heard yet on this network");
|
||||
return true;
|
||||
}
|
||||
if (name != "ping" && name != "traceroute" && name != "port" && name != "nslookup") return false;
|
||||
std::unique_ptr<Job> job(new Job());
|
||||
std::string why;
|
||||
if (name == "ping") {
|
||||
job->kind = Job::Kind::Ping;
|
||||
why = net::parsePing(args, job->ping);
|
||||
} else if (name == "traceroute") {
|
||||
job->kind = Job::Kind::Trace;
|
||||
why = net::parsePing(args, job->ping);
|
||||
if (!why.empty() || args.find(' ') != std::string::npos) why = "traceroute <host>";
|
||||
} else if (name == "port") {
|
||||
job->kind = Job::Kind::Port;
|
||||
why = net::parsePort(args, job->port);
|
||||
} else {
|
||||
job->kind = Job::Kind::Lookup;
|
||||
why = net::parseLookup(args, job->lookup);
|
||||
}
|
||||
if (!why.empty()) return console.printf("%s: %s\n", name.c_str(), why.c_str()), true;
|
||||
if (busy_) return console.printf("%s: another one is running: `cancel` stops it\n", name.c_str()), true;
|
||||
start(job.release());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "platform/console.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// The network troubleshooting commands (issue #90): ping, nslookup, port, traceroute, ifconfig,
|
||||
// arp. The first four take time, so each runs on a task of its own and prints its lines as they
|
||||
// come, to the console that asked; one at a time, and `cancel` stops it. The other two answer at
|
||||
// once. The packets and their meaning are lib/net/src/net_probe.h, which is host-tested.
|
||||
class NetTools {
|
||||
public:
|
||||
// True if `line` was one of its commands (answered, started, or refused with a reason).
|
||||
bool command(const std::string& line);
|
||||
bool busy() const { return busy_; }
|
||||
void cancel() { stop_ = true; }
|
||||
|
||||
private:
|
||||
struct Job;
|
||||
void start(Job* job);
|
||||
static void task(void* arg);
|
||||
|
||||
std::atomic<bool> busy_{false}, stop_{false};
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user