Files
roro9stack/lib/net/src/net_probe.cpp
T
twislaandClaude Opus 5.5 9808013fc0
CI / build (pull_request) Successful in 1m49s
Site / build (pull_request) Successful in 10s
Shell: tls, ntp and netstat (#90)
The rest of the issue's list. tls makes a handshake that checks nothing,
then says the certificate in words: who it is for, who signed it, until
when, and whether this device's roots and the name asked for accept it,
with the reason when they don't. ntp compares a time server's clock with
the device's. netstat lists what listens and what is connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-08 02:54:59 +02:00

301 lines
12 KiB
C++

#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;
}
void buildNtpRequest(uint8_t out[kNtpPacket]) {
std::memset(out, 0, kNtpPacket);
out[0] = 0x23; // no warning, version 4, a client
}
bool parseNtpAnswer(const uint8_t* p, size_t len, NtpAnswer& out) {
if (len < kNtpPacket || (p[0] & 0x07) != 4) return false; // not a server's
if (p[1] == 0 || p[1] > 15) return false; // "kiss of death", or not synchronised
uint32_t secs = (static_cast<uint32_t>(p[40]) << 24) | (p[41] << 16) | (p[42] << 8) | p[43];
uint32_t frac = (static_cast<uint32_t>(p[44]) << 24) | (p[45] << 16) | (p[46] << 8) | p[47];
if (!secs) return false;
// NTP counts from 1900 and wraps in 2036: a small number is the era after.
constexpr int64_t k1900To1970 = 2208988800LL;
int64_t since1900 = secs < 0x80000000u ? static_cast<int64_t>(secs) + 4294967296LL : static_cast<int64_t>(secs);
out.stratum = p[1];
out.seconds = since1900 - k1900To1970;
out.millis = static_cast<uint32_t>((static_cast<uint64_t>(frac) * 1000) >> 32);
return true;
}
std::string clockOffset(int64_t ownMs, int64_t serverMs) {
int64_t diff = ownMs - serverMs, size = diff < 0 ? -diff : diff;
if (size < 100) return "right, to 0.1 s";
std::string amount = size < 10000 ? std::to_string(size / 1000) + "." + std::to_string(size % 1000 / 100) + " s"
: size < 120000 ? std::to_string(size / 1000) + " s"
: size < 7200000 ? std::to_string(size / 60000) + " min"
: size < 172800000LL ? std::to_string(size / 3600000) + " h" : std::to_string(size / 86400000LL) + " days";
return amount + (diff > 0 ? " ahead" : " behind");
}
std::string certName(const std::string& dn) {
for (const char* key : {"CN=", "O="}) {
size_t at = 0;
while ((at = dn.find(key, at)) != std::string::npos) {
if (at == 0 || dn[at - 1] == ' ' || dn[at - 1] == ',') {
size_t from = at + std::strlen(key), end = dn.find(", ", from);
std::string name = dn.substr(from, end == std::string::npos ? std::string::npos : end - from);
// An old kind of string comes out as "#" and hex, type and length first: read it.
if (name.size() > 5 && name[0] == '#' && name.size() % 2 == 1) {
std::string plain;
for (size_t i = 5; i + 1 < name.size(); i += 2) {
auto digit = [](char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; };
int hi = digit(name[i]), lo = digit(name[i + 1]);
if (hi < 0 || lo < 0 || hi * 16 + lo < 0x20 || hi * 16 + lo > 0x7E) return name;
plain += static_cast<char>(hi * 16 + lo);
}
return plain;
}
return name;
}
at++;
}
}
return dn;
}
namespace {
// Days since a fixed day long ago (the civil calendar, leap years and all).
long dayNumber(int y, int m, int d) {
y -= m <= 2;
long era = (y >= 0 ? y : y - 399) / 400;
long yoe = y - era * 400, doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
return era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy;
}
} // namespace
int daysBetween(int y1, int m1, int d1, int y2, int m2, int d2) { return static_cast<int>(dayNumber(y2, m2, d2) - dayNumber(y1, m1, d1)); }
const char* portLabel(uint16_t port, bool tcp) {
if (tcp) return port == 3232 ? "updates" : port == 2323 ? "Debug Console" : port == 80 ? "sharing" : "";
return port == 68 ? "DHCP" : port == 123 ? "NTP" : "";
}
} // namespace roro::net