Public Access
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
This commit is contained in:
+139
-3
@@ -2,12 +2,18 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <NetworkClientSecure.h>
|
||||
|
||||
#include <lwip/etharp.h>
|
||||
#include <lwip/dns.h>
|
||||
#include <lwip/netdb.h>
|
||||
#include <lwip/netif.h>
|
||||
#include <lwip/priv/tcp_priv.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include <lwip/tcpip.h>
|
||||
#include <lwip/udp.h>
|
||||
#include <mbedtls/x509_crt.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <esp_random.h>
|
||||
|
||||
@@ -16,12 +22,15 @@
|
||||
|
||||
#include "ipv4.h"
|
||||
#include "net_probe.h"
|
||||
#include "platform/ca_roots.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
constexpr int kMaxHops = 20;
|
||||
constexpr uint32_t kPingEveryMs = 1000, kPingWaitMs = 1000, kHopWaitMs = 2000, kPortWaitMs = 5000, kDnsWaitMs = 3000;
|
||||
// A TLS handshake peaks at about 52 KB of heap; below this it isn't tried.
|
||||
constexpr size_t kTlsNeedsFree = 70 * 1024;
|
||||
|
||||
struct LwipLock {
|
||||
LwipLock() { LOCK_TCPIP_CORE(); }
|
||||
@@ -53,7 +62,7 @@ void waitMs(int socket, uint32_t ms) {
|
||||
} // namespace
|
||||
|
||||
struct NetTools::Job {
|
||||
enum class Kind { Ping, Trace, Port, Lookup } kind;
|
||||
enum class Kind { Ping, Trace, Port, Lookup, Tls, Ntp } kind;
|
||||
NetTools* owner;
|
||||
Console::Origin from;
|
||||
net::PingArgs ping;
|
||||
@@ -68,6 +77,8 @@ struct NetTools::Job {
|
||||
void runTrace();
|
||||
void runPort();
|
||||
void runLookup();
|
||||
void runTls();
|
||||
void runNtp();
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -223,6 +234,103 @@ void NetTools::Job::runLookup() {
|
||||
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" : "");
|
||||
}
|
||||
|
||||
// A handshake that checks nothing, to see the certificate whatever it is; then the certificate is
|
||||
// checked here, against this device's own roots and the name asked for, and the answer is said in
|
||||
// words. It is what the Update Service's connection would have decided.
|
||||
void NetTools::Job::runTls() {
|
||||
if (ESP.getFreeHeap() < kTlsNeedsFree)
|
||||
return (void)console.printf("tls: not enough memory (%u KB free, %u needed): close IRC or a Gemini page\n", (unsigned)(ESP.getFreeHeap() / 1024),
|
||||
(unsigned)(kTlsNeedsFree / 1024));
|
||||
NetworkClientSecure tls;
|
||||
tls.setInsecure();
|
||||
uint32_t started = millis();
|
||||
if (!tls.connect(port.host.c_str(), port.port, 8000)) {
|
||||
char why[100] = "";
|
||||
tls.lastError(why, sizeof why);
|
||||
return (void)console.printf("tls: no handshake with %s:%u in %lu ms: %s\n", port.host.c_str(), (unsigned)port.port, (unsigned long)(millis() - started),
|
||||
why[0] ? why : "no connection");
|
||||
}
|
||||
console.printf("tls: %s:%u answered in %lu ms\n", port.host.c_str(), (unsigned)port.port, (unsigned long)(millis() - started));
|
||||
const mbedtls_x509_crt* cert = tls.getPeerCertificate();
|
||||
if (!cert) {
|
||||
tls.stop();
|
||||
return (void)console.println("tls: it showed no certificate");
|
||||
}
|
||||
char dn[200];
|
||||
std::string subject = mbedtls_x509_dn_gets(dn, sizeof dn, &cert->subject) > 0 ? net::certName(dn) : "?";
|
||||
std::string issuer = mbedtls_x509_dn_gets(dn, sizeof dn, &cert->issuer) > 0 ? net::certName(dn) : "?";
|
||||
console.printf("tls: for %s, by %s\n", subject.c_str(), issuer.c_str());
|
||||
const mbedtls_x509_time& from = cert->valid_from;
|
||||
const mbedtls_x509_time& to = cert->valid_to;
|
||||
time_t now = time(nullptr);
|
||||
struct tm today;
|
||||
gmtime_r(&now, &today);
|
||||
bool clock = today.tm_year + 1900 >= 2024;
|
||||
int left = net::daysBetween(today.tm_year + 1900, today.tm_mon + 1, today.tm_mday, to.year, to.mon, to.day);
|
||||
console.printf("tls: valid %04d-%02d-%02d to %04d-%02d-%02d", from.year, from.mon, from.day, to.year, to.mon, to.day);
|
||||
if (!clock) console.println(" (this clock isn't set)");
|
||||
else if (left >= 0) console.printf(", %d days left\n", left);
|
||||
else console.printf(", EXPIRED %d days ago\n", -left);
|
||||
|
||||
mbedtls_x509_crt roots;
|
||||
mbedtls_x509_crt_init(&roots);
|
||||
uint32_t flags = 0;
|
||||
bool parsed = mbedtls_x509_crt_parse(&roots, reinterpret_cast<const unsigned char*>(kTrustedRootsPem), sizeof kTrustedRootsPem) == 0;
|
||||
int verdict = parsed ? mbedtls_x509_crt_verify(const_cast<mbedtls_x509_crt*>(cert), &roots, nullptr, port.host.c_str(), &flags, nullptr, nullptr) : -1;
|
||||
mbedtls_x509_crt_free(&roots);
|
||||
if (verdict == 0) console.println("tls: this device trusts it");
|
||||
else {
|
||||
std::string why;
|
||||
if ((flags & MBEDTLS_X509_BADCERT_EXPIRED) || (clock && left < 0)) why += ", expired";
|
||||
if (flags & MBEDTLS_X509_BADCERT_FUTURE) why += ", not valid yet";
|
||||
if (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH) why += ", not for that name";
|
||||
if (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED) why += ", not signed by a root this device has";
|
||||
if (why.empty()) why = ", it doesn't check out";
|
||||
console.printf("tls: NOT trusted here: %s\n", why.c_str() + 2);
|
||||
}
|
||||
uint8_t sha[32];
|
||||
if (tls.getFingerprintSHA256(sha)) {
|
||||
char hex[65];
|
||||
for (int i = 0; i < 32; i++) std::snprintf(hex + i * 2, 3, "%02x", sha[i]);
|
||||
console.printf("tls: sha256 %s\n", hex);
|
||||
}
|
||||
tls.stop();
|
||||
}
|
||||
|
||||
// Asks a time server and compares with this device's clock, allowing for half the round trip.
|
||||
void NetTools::Job::runNtp() {
|
||||
uint32_t to;
|
||||
if (!resolve(port.host, to)) return (void)console.printf("ntp: %s doesn't resolve\n", port.host.c_str());
|
||||
int s = lwip_socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s < 0) return (void)console.println("ntp: error no socket");
|
||||
uint8_t packet[net::kNtpPacket];
|
||||
net::buildNtpRequest(packet);
|
||||
struct sockaddr_in dest = {};
|
||||
dest.sin_family = AF_INET;
|
||||
dest.sin_port = lwip_htons(123);
|
||||
dest.sin_addr.s_addr = to;
|
||||
uint32_t sent = micros();
|
||||
net::NtpAnswer answer;
|
||||
bool got = false;
|
||||
struct timeval own = {};
|
||||
if (lwip_sendto(s, packet, sizeof packet, 0, reinterpret_cast<struct sockaddr*>(&dest), sizeof dest) >= 0) {
|
||||
while (!got && !stopped() && (micros() - sent) / 1000 < kDnsWaitMs) {
|
||||
waitMs(s, 200);
|
||||
int n = lwip_recv(s, packet, sizeof packet, 0);
|
||||
if (n <= 0) continue;
|
||||
gettimeofday(&own, nullptr);
|
||||
got = net::parseNtpAnswer(packet, static_cast<size_t>(n), answer);
|
||||
}
|
||||
}
|
||||
uint32_t tripMs = (micros() - sent) / 1000;
|
||||
lwip_close(s);
|
||||
if (!got) return (void)console.printf("ntp: no answer from %s (%s) in %lu s\n", port.host.c_str(), text(to).c_str(), (unsigned long)(kDnsWaitMs / 1000));
|
||||
console.printf("ntp: %s (%s), stratum %d, %lu ms away\n", port.host.c_str(), text(to).c_str(), answer.stratum, (unsigned long)tripMs);
|
||||
int64_t ownMs = static_cast<int64_t>(own.tv_sec) * 1000 + own.tv_usec / 1000, serverMs = answer.seconds * 1000 + answer.millis + tripMs / 2;
|
||||
if (own.tv_sec < 1700000000) console.println("ntp: this clock isn't set");
|
||||
else console.printf("ntp: this clock is %s\n", net::clockOffset(ownMs, serverMs).c_str());
|
||||
}
|
||||
|
||||
void NetTools::task(void* arg) {
|
||||
Job* job = static_cast<Job*>(arg);
|
||||
{
|
||||
@@ -232,6 +340,8 @@ void NetTools::task(void* arg) {
|
||||
case Job::Kind::Trace: job->runTrace(); break;
|
||||
case Job::Kind::Port: job->runPort(); break;
|
||||
case Job::Kind::Lookup: job->runLookup(); break;
|
||||
case Job::Kind::Tls: job->runTls(); break;
|
||||
case Job::Kind::Ntp: job->runNtp(); break;
|
||||
}
|
||||
}
|
||||
NetTools* owner = job->owner;
|
||||
@@ -245,7 +355,8 @@ void NetTools::start(Job* job) {
|
||||
job->from = console.origin();
|
||||
stop_ = false;
|
||||
busy_ = true;
|
||||
if (xTaskCreate(task, "nettool", 6144, job, 1, nullptr) != pdPASS) {
|
||||
// A TLS handshake needs far more stack than a ping.
|
||||
if (xTaskCreate(task, "nettool", job->kind == Job::Kind::Tls ? 12288 : 6144, job, 1, nullptr) != pdPASS) {
|
||||
busy_ = false;
|
||||
delete job;
|
||||
console.println("net: error not enough memory for it");
|
||||
@@ -289,7 +400,23 @@ bool NetTools::command(const std::string& line) {
|
||||
if (!found) console.println("arp: nobody heard yet on this network");
|
||||
return true;
|
||||
}
|
||||
if (name != "ping" && name != "traceroute" && name != "port" && name != "nslookup") return false;
|
||||
if (name == "netstat") {
|
||||
LwipLock lock;
|
||||
for (struct tcp_pcb_listen* p = tcp_listen_pcbs.listen_pcbs; p; p = p->next) {
|
||||
const char* label = net::portLabel(p->local_port, true);
|
||||
console.printf("netstat: tcp %u listens%s%s%s\n", (unsigned)p->local_port, *label ? " (" : "", label, *label ? ")" : "");
|
||||
}
|
||||
for (struct tcp_pcb* p = tcp_active_pcbs; p; p = p->next) {
|
||||
std::string local = IP_IS_V4(&p->local_ip) ? text(ip_2_ip4(&p->local_ip)->addr) : "::", remote = IP_IS_V4(&p->remote_ip) ? text(ip_2_ip4(&p->remote_ip)->addr) : "::";
|
||||
console.printf("netstat: tcp %s:%u - %s:%u\n", local.c_str(), (unsigned)p->local_port, remote.c_str(), (unsigned)p->remote_port);
|
||||
}
|
||||
for (struct udp_pcb* p = udp_pcbs; p; p = p->next) {
|
||||
const char* label = net::portLabel(p->local_port, false);
|
||||
console.printf("netstat: udp %u%s%s%s\n", (unsigned)p->local_port, *label ? " (" : "", label, *label ? ")" : "");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (name != "ping" && name != "traceroute" && name != "port" && name != "nslookup" && name != "tls" && name != "ntp") return false;
|
||||
std::unique_ptr<Job> job(new Job());
|
||||
std::string why;
|
||||
if (name == "ping") {
|
||||
@@ -302,6 +429,15 @@ bool NetTools::command(const std::string& line) {
|
||||
} else if (name == "port") {
|
||||
job->kind = Job::Kind::Port;
|
||||
why = net::parsePort(args, job->port);
|
||||
} else if (name == "tls") { // the port may be left out: 443
|
||||
job->kind = Job::Kind::Tls;
|
||||
bool onlyHost = !args.empty() && args.find(' ') == std::string::npos && args.find(':') == std::string::npos;
|
||||
why = net::parsePort(onlyHost ? args + " 443" : args, job->port);
|
||||
if (!why.empty() && why.find("port <") == 0) why = "tls <host> [port]";
|
||||
} else if (name == "ntp") { // the server may be left out: the first one in use
|
||||
job->kind = Job::Kind::Ntp;
|
||||
job->port.host = !args.empty() ? args : ntpServer ? ntpServer() : "";
|
||||
if (job->port.host.empty() || !net::validHost(job->port.host)) why = "ntp [server]";
|
||||
} else {
|
||||
job->kind = Job::Kind::Lookup;
|
||||
why = net::parseLookup(args, job->lookup);
|
||||
|
||||
Reference in New Issue
Block a user