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:
@@ -0,0 +1,227 @@
|
||||
#include "net_probe.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "ipv4.h"
|
||||
|
||||
namespace roro::net {
|
||||
|
||||
namespace {
|
||||
std::vector<std::string> words(const std::string& text) {
|
||||
std::vector<std::string> out;
|
||||
size_t at = 0;
|
||||
while (at < text.size()) {
|
||||
while (at < text.size() && text[at] == ' ') at++;
|
||||
size_t end = text.find(' ', at);
|
||||
if (end == std::string::npos) end = text.size();
|
||||
if (end > at) out.push_back(text.substr(at, end - at));
|
||||
at = end;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
bool number(const std::string& s, long& out) {
|
||||
if (s.empty() || s.size() > 6) return false;
|
||||
out = 0;
|
||||
for (char c : s) {
|
||||
if (c < '0' || c > '9') return false;
|
||||
out = out * 10 + (c - '0');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint16_t be16(const uint8_t* p) { return static_cast<uint16_t>((p[0] << 8) | p[1]); }
|
||||
} // namespace
|
||||
|
||||
std::string parsePing(const std::string& args, PingArgs& out) {
|
||||
static const char* const kUsage = "ping <host> [count] [size]";
|
||||
auto w = words(args);
|
||||
if (w.empty() || w.size() > 3 || !validHost(w[0])) return kUsage;
|
||||
PingArgs a;
|
||||
a.host = w[0];
|
||||
long n;
|
||||
if (w.size() > 1) {
|
||||
if (!number(w[1], n) || n < 1 || n > 100) return "a count from 1 to 100";
|
||||
a.count = static_cast<int>(n);
|
||||
}
|
||||
if (w.size() > 2) {
|
||||
if (!number(w[2], n) || n > 1400) return "a size from 0 to 1400 bytes";
|
||||
a.size = static_cast<int>(n);
|
||||
}
|
||||
out = a;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string parsePort(const std::string& args, PortArgs& out) {
|
||||
static const char* const kUsage = "port <host> <port>";
|
||||
auto w = words(args);
|
||||
if (w.size() == 1) { // host:port
|
||||
size_t colon = w[0].rfind(':');
|
||||
if (colon == std::string::npos) return kUsage;
|
||||
w = {w[0].substr(0, colon), w[0].substr(colon + 1)};
|
||||
}
|
||||
long n;
|
||||
if (w.size() != 2 || !validHost(w[0])) return kUsage;
|
||||
if (!number(w[1], n) || n < 1 || n > 65535) return "a port from 1 to 65535";
|
||||
out.host = w[0];
|
||||
out.port = static_cast<uint16_t>(n);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string parseLookup(const std::string& args, LookupArgs& out) {
|
||||
static const char* const kUsage = "nslookup <name> [server's address]";
|
||||
auto w = words(args);
|
||||
uint32_t ip;
|
||||
if (w.empty() || w.size() > 2 || !validHost(w[0])) return kUsage;
|
||||
if (w.size() == 2 && !parseIpv4(w[1], ip)) return "the server as an address: 9.9.9.9";
|
||||
out.name = w[0];
|
||||
out.server = w.size() == 2 ? w[1] : "";
|
||||
return "";
|
||||
}
|
||||
|
||||
uint16_t inetChecksum(const uint8_t* data, size_t len) {
|
||||
uint32_t sum = 0;
|
||||
for (size_t i = 0; i + 1 < len; i += 2) sum += static_cast<uint32_t>((data[i] << 8) | data[i + 1]);
|
||||
if (len & 1) sum += static_cast<uint32_t>(data[len - 1] << 8);
|
||||
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
return static_cast<uint16_t>(~sum);
|
||||
}
|
||||
|
||||
size_t buildEcho(uint8_t* out, size_t max, uint16_t id, uint16_t seq, size_t payload) {
|
||||
size_t len = 8 + payload;
|
||||
if (len > max) return 0;
|
||||
out[0] = 8; // echo request
|
||||
out[1] = 0;
|
||||
out[2] = out[3] = 0;
|
||||
out[4] = static_cast<uint8_t>(id >> 8);
|
||||
out[5] = static_cast<uint8_t>(id);
|
||||
out[6] = static_cast<uint8_t>(seq >> 8);
|
||||
out[7] = static_cast<uint8_t>(seq);
|
||||
for (size_t i = 0; i < payload; i++) out[8 + i] = static_cast<uint8_t>('a' + i % 26);
|
||||
uint16_t sum = inetChecksum(out, len);
|
||||
out[2] = static_cast<uint8_t>(sum >> 8);
|
||||
out[3] = static_cast<uint8_t>(sum);
|
||||
return len;
|
||||
}
|
||||
|
||||
IcmpAnswer parseIcmp(const uint8_t* packet, size_t len) {
|
||||
IcmpAnswer a;
|
||||
if (len < 20 || (packet[0] >> 4) != 4) return a;
|
||||
size_t header = static_cast<size_t>(packet[0] & 0x0F) * 4;
|
||||
if (header < 20 || len < header + 8 || packet[9] != 1) return a; // not ICMP
|
||||
const uint8_t* icmp = packet + header;
|
||||
size_t left = len - header;
|
||||
if (icmp[0] == 0 && icmp[1] == 0) { // echo reply
|
||||
a.kind = IcmpAnswer::Kind::Echo;
|
||||
a.id = be16(icmp + 4);
|
||||
a.seq = be16(icmp + 6);
|
||||
return a;
|
||||
}
|
||||
if (icmp[0] != 11 && icmp[0] != 3) return a;
|
||||
// Inside: the IP header of the packet it is about, and that packet's first 8 bytes.
|
||||
if (left < 8 + 20) return a;
|
||||
const uint8_t* inner = icmp + 8;
|
||||
size_t innerHeader = static_cast<size_t>(inner[0] & 0x0F) * 4;
|
||||
if ((inner[0] >> 4) != 4 || innerHeader < 20 || left < 8 + innerHeader + 8 || inner[9] != 1 || inner[innerHeader] != 8) return a;
|
||||
a.kind = icmp[0] == 11 ? IcmpAnswer::Kind::TimeExceeded : IcmpAnswer::Kind::Unreachable;
|
||||
a.id = be16(inner + innerHeader + 4);
|
||||
a.seq = be16(inner + innerHeader + 6);
|
||||
return a;
|
||||
}
|
||||
|
||||
void PingStats::add(uint32_t ms) {
|
||||
minMs = back ? std::min(minMs, ms) : ms;
|
||||
maxMs = std::max(maxMs, ms);
|
||||
sumMs += ms;
|
||||
back++;
|
||||
}
|
||||
|
||||
std::string PingStats::summary() const {
|
||||
int lost = sent ? (sent - back) * 100 / sent : 0;
|
||||
std::string s = std::to_string(back) + "/" + std::to_string(sent) + " back, " + std::to_string(lost) + "% lost"; // short: a Shell line is 38 characters
|
||||
if (back) s += ", " + std::to_string(minMs) + "/" + std::to_string(sumMs / static_cast<uint32_t>(back)) + "/" + std::to_string(maxMs) + " ms";
|
||||
return s;
|
||||
}
|
||||
|
||||
size_t buildDnsQuery(uint8_t* out, size_t max, uint16_t id, const std::string& name) {
|
||||
if (name.empty() || name.size() > 253 || 12 + name.size() + 2 + 4 > max) return 0;
|
||||
std::memset(out, 0, 12);
|
||||
out[0] = static_cast<uint8_t>(id >> 8);
|
||||
out[1] = static_cast<uint8_t>(id);
|
||||
out[2] = 0x01; // recursion wanted
|
||||
out[5] = 1; // one question
|
||||
size_t at = 12;
|
||||
for (size_t from = 0; from <= name.size();) {
|
||||
size_t dot = name.find('.', from);
|
||||
if (dot == std::string::npos) dot = name.size();
|
||||
size_t n = dot - from;
|
||||
if (n == 0 && dot == name.size()) break; // a final dot
|
||||
if (n == 0 || n > 63) return 0;
|
||||
out[at++] = static_cast<uint8_t>(n);
|
||||
std::memcpy(out + at, name.data() + from, n);
|
||||
at += n;
|
||||
from = dot + 1;
|
||||
}
|
||||
out[at++] = 0;
|
||||
out[at++] = 0;
|
||||
out[at++] = 1; // A
|
||||
out[at++] = 0;
|
||||
out[at++] = 1; // IN
|
||||
return at;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Reads a name at `at`, following the pointers DNS shortens names with. Where the name ends in
|
||||
// the message (not where a pointer led), or 0 if it is broken.
|
||||
size_t readName(const uint8_t* m, size_t len, size_t at, std::string* out) {
|
||||
size_t end = 0;
|
||||
int jumps = 0;
|
||||
while (at < len) {
|
||||
uint8_t n = m[at];
|
||||
if (n == 0) return end ? end : at + 1;
|
||||
if ((n & 0xC0) == 0xC0) {
|
||||
if (at + 1 >= len || ++jumps > 8) return 0;
|
||||
if (!end) end = at + 2;
|
||||
at = static_cast<size_t>(((n & 0x3F) << 8) | m[at + 1]);
|
||||
continue;
|
||||
}
|
||||
if (n > 63 || at + 1 + n > len) return 0;
|
||||
if (out) {
|
||||
if (!out->empty()) *out += '.';
|
||||
out->append(reinterpret_cast<const char*>(m + at + 1), n);
|
||||
}
|
||||
at += 1 + static_cast<size_t>(n);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool parseDnsAnswer(const uint8_t* m, size_t len, uint16_t id, DnsAnswer& out) {
|
||||
if (len < 12 || be16(m) != id || !(m[2] & 0x80)) return false;
|
||||
DnsAnswer a;
|
||||
a.truncated = m[2] & 0x02;
|
||||
a.rcode = m[3] & 0x0F;
|
||||
int questions = be16(m + 4), answers = be16(m + 6);
|
||||
size_t at = 12;
|
||||
for (int i = 0; i < questions; i++) {
|
||||
at = readName(m, len, at, nullptr);
|
||||
if (!at || at + 4 > len) return false;
|
||||
at += 4;
|
||||
}
|
||||
for (int i = 0; i < answers; i++) {
|
||||
at = readName(m, len, at, nullptr);
|
||||
if (!at || at + 10 > len) return false;
|
||||
uint16_t type = be16(m + at), size = be16(m + at + 8);
|
||||
at += 10;
|
||||
if (at + size > len) return false;
|
||||
if (type == 1 && size == 4) a.addresses.push_back((static_cast<uint32_t>(m[at]) << 24) | (m[at + 1] << 16) | (m[at + 2] << 8) | m[at + 3]);
|
||||
if (type == 5) {
|
||||
std::string name;
|
||||
if (readName(m, len, at, &name)) a.alias = name;
|
||||
}
|
||||
at += size;
|
||||
}
|
||||
out = a;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace roro::net
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user