Shell: ping, nslookup, port, traceroute, ifconfig and arp (#90)
CI / build (pull_request) Successful in 1m38s
Site / build (pull_request) Successful in 11s

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:
2026-10-08 02:29:04 +02:00
co-authored by Claude Opus 5.5
parent 86172c0342
commit ed7abdcaf5
16 changed files with 969 additions and 6 deletions
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
// The parts of the network troubleshooting commands (issue #90) that need no network: what was
// asked, the packets to send, and what the answers mean. The sockets are src/services/net_tools.h (a different name on purpose: two headers of one name find themselves).
namespace roro::net {
// --- what was typed
struct PingArgs {
std::string host;
int count = 4; // 1 to 100
int size = 56; // bytes of payload, 0 to 1400: with its headers a ping is 28 more
};
// "ping <host> [count] [size]". "" or how to ask.
std::string parsePing(const std::string& args, PingArgs& out);
struct PortArgs {
std::string host;
uint16_t port = 0;
};
// "port <host> <port>", or host:port.
std::string parsePort(const std::string& args, PortArgs& out);
struct LookupArgs {
std::string name, server; // server: an address, or "" for the one in use
};
std::string parseLookup(const std::string& args, LookupArgs& out);
// --- ICMP: ping and traceroute
uint16_t inetChecksum(const uint8_t* data, size_t len);
// An echo request: 8 bytes of header and `payload` bytes after it. The length written, or 0 if
// it doesn't fit.
size_t buildEcho(uint8_t* out, size_t max, uint16_t id, uint16_t seq, size_t payload);
struct IcmpAnswer {
enum class Kind { Other, Echo, TimeExceeded, Unreachable } kind = Kind::Other;
uint16_t id = 0, seq = 0; // of the echo request it answers
};
// `packet` as a raw socket hands it over: the IP header first. A router's "time exceeded" and
// "unreachable" carry the start of the packet they are about, which is where id and seq come from.
IcmpAnswer parseIcmp(const uint8_t* packet, size_t len);
// What a run of pings came to: "3/4 back, 25% lost, 12/25/41 ms" (the least, the mean, the most).
struct PingStats {
int sent = 0, back = 0;
uint32_t minMs = 0, maxMs = 0, sumMs = 0;
void add(uint32_t ms);
std::string summary() const;
};
// --- DNS: nslookup
// A query for the IPv4 addresses of `name`. The length written, or 0 if that isn't a name.
size_t buildDnsQuery(uint8_t* out, size_t max, uint16_t id, const std::string& name);
struct DnsAnswer {
int rcode = 0; // 0: fine, 3: no such name
bool truncated = false; // the answer didn't fit in one packet
std::vector<uint32_t> addresses;
std::string alias; // the last name a CNAME led to, if any
};
// False if it isn't the answer to query `id`, or is cut short.
bool parseDnsAnswer(const uint8_t* message, size_t len, uint16_t id, DnsAnswer& out);
} // namespace roro::net