Public Access
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8f267416b | ||
|
|
ed7abdcaf5 | ||
|
|
86172c0342 |
@@ -13,7 +13,7 @@ What it does today:
|
|||||||
- **Wi-Fi Tools:** the networks around, sorted, filtered, logged.
|
- **Wi-Fi Tools:** the networks around, sorted, filtered, logged.
|
||||||
- **Notes:** plain text files of any size, saved by themselves.
|
- **Notes:** plain text files of any size, saved by themselves.
|
||||||
- **Storage:** the SD card: copy, move, rename, delete; viewers for text, hex, pictures (PNG, JPEG, BMP, GIF), tracks, captures and update files; **sharing with a phone's browser**.
|
- **Storage:** the SD card: copy, move, rename, delete; viewers for text, hex, pictures (PNG, JPEG, BMP, GIF), tracks, captures and update files; **sharing with a phone's browser**.
|
||||||
- **Shell:** the firmware's commands on the device itself, with completion.
|
- **Shell:** the firmware's commands on the device itself, with completion, including `ping`, `nslookup`, `port`, `traceroute` and `ifconfig`.
|
||||||
- **System:** load, tasks, memory, network, battery, live.
|
- **System:** load, tasks, memory, network, battery, live.
|
||||||
- **VPN:** a WireGuard tunnel.
|
- **VPN:** a WireGuard tunnel.
|
||||||
- **Everywhere:** Fn+h lists the keys of the screen you are on; Fn+p takes a screenshot.
|
- **Everywhere:** Fn+h lists the keys of the screen you are on; Fn+p takes a screenshot.
|
||||||
@@ -185,7 +185,7 @@ A WireGuard tunnel (docs/milestones/N1.md), over whatever Wi-Fi the device is on
|
|||||||
|
|
||||||
## Shell
|
## Shell
|
||||||
|
|
||||||
The Shell App (docs/milestones/S1.md) runs the commands below on the device's own screen and keyboard: no PC, no cable, no Wi-Fi. **It shows the replies to its own commands and nothing else**: the console knows who each line is printed for, so a listing read by another task a moment later is still the Shell's, and what USB or the Debug Console asked for is not. Ctrl+b shows everything instead. Tab completes a command word by word (`lora st` gives `lora status`) and, past it, a path on the SD card (`ls /no` gives `ls /notes/`), Fn with up and down recalls earlier lines, Alt with up and down scrolls back. **An App's name with a capital opens it** (`Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `System`, `Settings`), from the consoles too. `rm` is Unix's, with a question: a folder needs `-r`; a file, or a folder with something in it, is asked about unless `-f` (`rm -rf`); an empty folder goes without a word. `*` and `?` in a name stand for several files (`rm /notes/*.txt` asks once, with the count; 64 at most). `clear` empties the screen and `quit` leaves. It is trusted like the USB port: `debug on` and `debug token` work from it. It uses about 7 KB of memory while it is open, and none otherwise.
|
The Shell App (docs/milestones/S1.md) runs the commands below on the device's own screen and keyboard: no PC, no cable, no Wi-Fi. **It shows the replies to its own commands and nothing else**: the console knows who each line is printed for, so a listing read by another task a moment later is still the Shell's, and what USB or the Debug Console asked for is not. Ctrl+b shows everything instead. Tab completes a command word by word (`lora st` gives `lora status`) and, past it, a path on the SD card (`ls /no` gives `ls /notes/`), Fn with up and down recalls earlier lines, Alt with up and down scrolls back. **An App's name with a capital opens it** (`Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `System`, `Settings`), from the consoles too. `rm` is Unix's, with a question: a folder needs `-r`; a file, or a folder with something in it, is asked about unless `-f` (`rm -rf`); an empty folder goes without a word. `*` and `?` in a name stand for several files (`rm /notes/*.txt` asks once, with the count; 64 at most). `clear` empties the screen and `quit` leaves. It is trusted like the USB port: `debug on` and `debug token` work from it. It uses about 7 KB of memory while it is open, and none otherwise. **For the network:** `ping`, `nslookup`, `port`, `traceroute`, `ifconfig` and `arp` (issue #90).
|
||||||
|
|
||||||
## Development aids
|
## Development aids
|
||||||
|
|
||||||
@@ -241,6 +241,8 @@ The Shell App (docs/milestones/S1.md) runs the commands below on the device's ow
|
|||||||
| `coredump erase` | Forgets the core dump |
|
| `coredump erase` | Forgets the core dump |
|
||||||
| `loop spin on` / `loop spin off` | Make the main loop spin without resting, to compare load and radio noise |
|
| `loop spin on` / `loop spin off` | Make the main loop spin without resting, to compare load and radio noise |
|
||||||
| `crash abort` / `crash wdt` | Crash on purpose, or hang the main loop until the watchdog fires |
|
| `crash abort` / `crash wdt` | Crash on purpose, or hang the main loop until the watchdog fires |
|
||||||
|
| `ping <host> [count] [size]` / `nslookup <name> [server]` / `port <host> <port>` / `traceroute <host>` / `cancel` | Network troubleshooting (issue #90): does a host answer and how fast; a name's addresses, from which DNS server and in how long; is a TCP port open, refused or silent; the routers on the way. Each runs on a task of its own and prints as it goes, one at a time; `cancel` stops it |
|
||||||
|
| `ifconfig` / `arp` | The interfaces (Wi-Fi and the VPN) with their addresses, MTU, which is the default route, and the DNS servers; the neighbours heard on the Wi-Fi |
|
||||||
| `vpn status` / `vpn up [seconds]` / `vpn down` / `vpn import [path]` / `vpn forget` / `vpn auto on\|off` | The WireGuard tunnel: its state, on (for that many seconds, then off by itself: for trying a configuration from afar), off, read a `.conf` from the card (`/vpn/wg0.conf`), erase it, start with Wi-Fi. No key is ever printed |
|
| `vpn status` / `vpn up [seconds]` / `vpn down` / `vpn import [path]` / `vpn forget` / `vpn auto on\|off` | The WireGuard tunnel: its state, on (for that many seconds, then off by itself: for trying a configuration from afar), off, read a `.conf` from the card (`/vpn/wg0.conf`), erase it, start with Wi-Fi. No key is ever printed |
|
||||||
| `debug status` / `debug off [seconds]` | The Debug Console: whether it's on, has a token and a client; switch it off. With a number of seconds, it comes back by itself after that long |
|
| `debug status` / `debug off [seconds]` | The Debug Console: whether it's on, has a token and a client; switch it off. With a number of seconds, it comes back by itself after that long |
|
||||||
| `debug on` / `debug token <value>` / `debug token new` | USB serial only: switch it on (making a token if there's none), give it a token of 16 to 64 characters, or make a new one. The token is never printed |
|
| `debug on` / `debug token <value>` / `debug token new` | USB serial only: switch it on (making a token if there's none), give it a token of 16 to 64 characters, or make a new one. The token is never printed |
|
||||||
|
|||||||
+41
-1
@@ -1,6 +1,6 @@
|
|||||||
# N1 — Network tools
|
# N1 — Network tools
|
||||||
|
|
||||||
**Status:** in progress. The WireGuard tunnel (issue #8) shipped as **v0.19.0**. SSH (#2) is not started.
|
**Status:** in progress. The WireGuard tunnel (issue #8) shipped as **v0.19.0**. The first network troubleshooting commands (issue #90) shipped as **v0.20.0**: `ping`, `nslookup`, `port`, `traceroute`, `ifconfig`, `arp`; `tls`, `ntp` and `netstat` are still to do. SSH (#2) is not started.
|
||||||
|
|
||||||
**Goal:** reach things from the device that aren't on the Wi-Fi it happens to be on, and keep its traffic private on a network that isn't yours.
|
**Goal:** reach things from the device that aren't on the Wi-Fi it happens to be on, and keep its traffic private on a network that isn't yours.
|
||||||
|
|
||||||
@@ -81,3 +81,43 @@ The test keys were made for the purpose and deleted. Two rounds: first with the
|
|||||||
**Taking the tunnel down reconnected Wi-Fi.** The first version gave DHCP's DNS servers back by asking for a new lease, which is how the Wi-Fi settings do it, and which drops every connection: the Debug Console session that had typed `vpn down` among them. The servers that were there are now simply remembered and put back.
|
**Taking the tunnel down reconnected Wi-Fi.** The first version gave DHCP's DNS servers back by asking for a new lease, which is how the Wi-Fi settings do it, and which drops every connection: the Debug Console session that had typed `vpn down` among them. The servers that were there are now simply remembered and put back.
|
||||||
|
|
||||||
**"What AllowedIPs say" was more than the network stack can do.** The design round promised split tunnels by AllowedIPs. lwIP has no routing table: it can send by an interface's subnet, or by default. So it is one subnet or everything, and the import tells which.
|
**"What AllowedIPs say" was more than the network stack can do.** The design round promised split tunnels by AllowedIPs. lwIP has no routing table: it can send by an interface's subnet, or by default. So it is one subnet or everything, and the import tells which.
|
||||||
|
|
||||||
|
## Network troubleshooting commands (issue #90)
|
||||||
|
|
||||||
|
With a tunnel, fixed addresses and a file server on the device, "is it the network or is it me" needed another machine to answer.
|
||||||
|
|
||||||
|
### Decisions (2026-10-08; built on the issue's list, without a round of questions)
|
||||||
|
|
||||||
|
- **The familiar names:** `ping`, `nslookup`, `traceroute`, `ifconfig`, `arp`. `port <host> <port>` for "is that TCP port open", which has no single familiar name.
|
||||||
|
- **In this version:** those six. **Not yet:** `tls` (why a certificate fails), `ntp` (the clock's offset), `netstat` (what listens). The issue stays open for them.
|
||||||
|
- **Commands only,** in the Shell and over both consoles; no page in an App.
|
||||||
|
- **One line an answer, short:** a Shell line is 38 characters.
|
||||||
|
|
||||||
|
### As built
|
||||||
|
|
||||||
|
- **`lib/net/src/net_probe.h`** (host-tested, 5 tests): what was typed; the ICMP echo request and what answers it, a router's "time exceeded" included; the DNS query and its answer, with the pointers names are shortened by.
|
||||||
|
- **`NetTools`** (`src/services/net_tools`): `ping`, `nslookup`, `port` and `traceroute` each run on a task of their own, made for the command and gone after it, printing to the console that asked (the Shell shows only its own replies). One at a time; `cancel` stops it within a fifth of a second.
|
||||||
|
- **`ping`** and **`traceroute`** share a raw ICMP socket: a traceroute is echo requests allowed one hop, then two, then three, and the routers' complaints are the list.
|
||||||
|
- **`nslookup`** asks one server itself, over UDP, and so can say which server answered and how long it took, which the system's resolver doesn't; and it can ask a server that isn't the configured one.
|
||||||
|
- **`port`** is a connection attempt that is not waited for: open, refused, or five seconds of nothing.
|
||||||
|
- **`ifconfig`** and **`arp`** read lwIP's own lists, with its lock held.
|
||||||
|
- **Cost:** 12 KB of flash. A 6 KB task while a command runs (2.6 KB of it never used), nothing otherwise.
|
||||||
|
|
||||||
|
### Checks on the device (2026-10-08, with the VPN up and everything routed through it)
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| `ifconfig` | `vpn 10.9.0.2/32 mtu 1420, up, default route`; `wifi ... gw ... mtu 1500, up`; the DNS server |
|
||||||
|
| `arp` | The gateway and one other machine |
|
||||||
|
| `ping` of a neighbour, of a name | 4 of 4 in 3 to 4 ms; 3 of 3 in about 50 ms |
|
||||||
|
| `ping 9.9.9.9 2 1392`, then `1393` | Both back; neither back: the tunnel carries 1420 bytes exactly |
|
||||||
|
| `nslookup` | The address, the server and the time; an alias followed; with another server; "there is no nope.invalid" |
|
||||||
|
| `port` | `open, 52 ms`; `refused`; "no answer in 5 s"; "doesn't resolve" |
|
||||||
|
| `traceroute 9.9.9.9` | Nine hops, the tunnel's server first, "arrived" |
|
||||||
|
| A second command while a ping runs | "another one is running: `cancel` stops it" |
|
||||||
|
| `cancel` | "stopped", with the count so far |
|
||||||
|
| In the Shell | Tab completes them; the lines appear there and only there |
|
||||||
|
|
||||||
|
**Not checked:** without the VPN (every check went through the tunnel, or to the local network); a network that drops ICMP; the commands in Safe Mode, where they are not offered.
|
||||||
|
|
||||||
|
**Found on the way:** a refused connection is reported by lwIP as "reset", not "refused"; the first version called it "no route". And the header for the tested half was first given the same name as the service's, which makes a file include itself: the same mistake as an hour before, in the same way.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -39,6 +39,8 @@ install <path.ota> Update from SD
|
|||||||
update check | update list | update status | update install <tag> the project's releases on Gitea
|
update check | update list | update status | update install <tag> the project's releases on Gitea
|
||||||
sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal
|
sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal
|
||||||
Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command
|
Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command
|
||||||
|
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
|
||||||
|
ifconfig | arp the interfaces (Wi-Fi and the VPN), their addresses, the default route and the DNS servers; the neighbours heard
|
||||||
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
|
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
|
||||||
debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back
|
debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back
|
||||||
debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token
|
debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token
|
||||||
@@ -110,6 +112,8 @@ In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few r
|
|||||||
| `coredump erase` | Forgets the core dump |
|
| `coredump erase` | Forgets the core dump |
|
||||||
| `loop spin on` / `loop spin off` | Make the main loop spin without resting, to compare load and radio noise |
|
| `loop spin on` / `loop spin off` | Make the main loop spin without resting, to compare load and radio noise |
|
||||||
| `crash abort` / `crash wdt` | Crash on purpose, or hang the main loop until the watchdog fires |
|
| `crash abort` / `crash wdt` | Crash on purpose, or hang the main loop until the watchdog fires |
|
||||||
|
| `ping <host> [count] [size]` / `nslookup <name> [server]` / `port <host> <port>` / `traceroute <host>` / `cancel` | Network troubleshooting (issue #90): does a host answer and how fast; a name's addresses, from which DNS server and in how long; is a TCP port open, refused or silent; the routers on the way. Each runs on a task of its own and prints as it goes, one at a time; `cancel` stops it |
|
||||||
|
| `ifconfig` / `arp` | The interfaces (Wi-Fi and the VPN) with their addresses, MTU, which is the default route, and the DNS servers; the neighbours heard on the Wi-Fi |
|
||||||
| `vpn status` / `vpn up [seconds]` / `vpn down` / `vpn import [path]` / `vpn forget` / `vpn auto on\|off` | The WireGuard tunnel: its state, on (for that many seconds, then off by itself: for trying a configuration from afar), off, read a `.conf` from the card (`/vpn/wg0.conf`), erase it, start with Wi-Fi. No key is ever printed |
|
| `vpn status` / `vpn up [seconds]` / `vpn down` / `vpn import [path]` / `vpn forget` / `vpn auto on\|off` | The WireGuard tunnel: its state, on (for that many seconds, then off by itself: for trying a configuration from afar), off, read a `.conf` from the card (`/vpn/wg0.conf`), erase it, start with Wi-Fi. No key is ever printed |
|
||||||
| `debug status` / `debug off [seconds]` | The Debug Console: whether it's on, has a token and a client; switch it off. With a number of seconds, it comes back by itself after that long |
|
| `debug status` / `debug off [seconds]` | The Debug Console: whether it's on, has a token and a client; switch it off. With a number of seconds, it comes back by itself after that long |
|
||||||
| `debug on` / `debug token <value>` / `debug token new` | USB serial only: switch it on (making a token if there's none), give it a token of 16 to 64 characters, or make a new one. The token is never printed |
|
| `debug on` / `debug token <value>` / `debug token new` | USB serial only: switch it on (making a token if there's none), give it a token of 16 to 64 characters, or make a new one. The token is never printed |
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ docs = true
|
|||||||
source = "docs/milestones/N1.md"
|
source = "docs/milestones/N1.md"
|
||||||
tag = "N1"
|
tag = "N1"
|
||||||
+++
|
+++
|
||||||
**Status:** in progress. The WireGuard tunnel (issue #8) shipped as **v0.19.0**. SSH (#2) is not started.
|
**Status:** in progress. The WireGuard tunnel (issue #8) shipped as **v0.19.0**. The first network troubleshooting commands (issue #90) shipped as **v0.20.0**: `ping`, `nslookup`, `port`, `traceroute`, `ifconfig`, `arp`; `tls`, `ntp` and `netstat` are still to do. SSH (#2) is not started.
|
||||||
|
|
||||||
**Goal:** reach things from the device that aren't on the Wi-Fi it happens to be on, and keep its traffic private on a network that isn't yours.
|
**Goal:** reach things from the device that aren't on the Wi-Fi it happens to be on, and keep its traffic private on a network that isn't yours.
|
||||||
|
|
||||||
@@ -89,3 +89,43 @@ The test keys were made for the purpose and deleted. Two rounds: first with the
|
|||||||
**Taking the tunnel down reconnected Wi-Fi.** The first version gave DHCP's DNS servers back by asking for a new lease, which is how the Wi-Fi settings do it, and which drops every connection: the Debug Console session that had typed `vpn down` among them. The servers that were there are now simply remembered and put back.
|
**Taking the tunnel down reconnected Wi-Fi.** The first version gave DHCP's DNS servers back by asking for a new lease, which is how the Wi-Fi settings do it, and which drops every connection: the Debug Console session that had typed `vpn down` among them. The servers that were there are now simply remembered and put back.
|
||||||
|
|
||||||
**"What AllowedIPs say" was more than the network stack can do.** The design round promised split tunnels by AllowedIPs. lwIP has no routing table: it can send by an interface's subnet, or by default. So it is one subnet or everything, and the import tells which.
|
**"What AllowedIPs say" was more than the network stack can do.** The design round promised split tunnels by AllowedIPs. lwIP has no routing table: it can send by an interface's subnet, or by default. So it is one subnet or everything, and the import tells which.
|
||||||
|
|
||||||
|
## Network troubleshooting commands (issue #90)
|
||||||
|
|
||||||
|
With a tunnel, fixed addresses and a file server on the device, "is it the network or is it me" needed another machine to answer.
|
||||||
|
|
||||||
|
### Decisions (2026-10-08; built on the issue's list, without a round of questions)
|
||||||
|
|
||||||
|
- **The familiar names:** `ping`, `nslookup`, `traceroute`, `ifconfig`, `arp`. `port <host> <port>` for "is that TCP port open", which has no single familiar name.
|
||||||
|
- **In this version:** those six. **Not yet:** `tls` (why a certificate fails), `ntp` (the clock's offset), `netstat` (what listens). The issue stays open for them.
|
||||||
|
- **Commands only,** in the Shell and over both consoles; no page in an App.
|
||||||
|
- **One line an answer, short:** a Shell line is 38 characters.
|
||||||
|
|
||||||
|
### As built
|
||||||
|
|
||||||
|
- **`lib/net/src/net_probe.h`** (host-tested, 5 tests): what was typed; the ICMP echo request and what answers it, a router's "time exceeded" included; the DNS query and its answer, with the pointers names are shortened by.
|
||||||
|
- **`NetTools`** (`src/services/net_tools`): `ping`, `nslookup`, `port` and `traceroute` each run on a task of their own, made for the command and gone after it, printing to the console that asked (the Shell shows only its own replies). One at a time; `cancel` stops it within a fifth of a second.
|
||||||
|
- **`ping`** and **`traceroute`** share a raw ICMP socket: a traceroute is echo requests allowed one hop, then two, then three, and the routers' complaints are the list.
|
||||||
|
- **`nslookup`** asks one server itself, over UDP, and so can say which server answered and how long it took, which the system's resolver doesn't; and it can ask a server that isn't the configured one.
|
||||||
|
- **`port`** is a connection attempt that is not waited for: open, refused, or five seconds of nothing.
|
||||||
|
- **`ifconfig`** and **`arp`** read lwIP's own lists, with its lock held.
|
||||||
|
- **Cost:** 12 KB of flash. A 6 KB task while a command runs (2.6 KB of it never used), nothing otherwise.
|
||||||
|
|
||||||
|
### Checks on the device (2026-10-08, with the VPN up and everything routed through it)
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| `ifconfig` | `vpn 10.9.0.2/32 mtu 1420, up, default route`; `wifi ... gw ... mtu 1500, up`; the DNS server |
|
||||||
|
| `arp` | The gateway and one other machine |
|
||||||
|
| `ping` of a neighbour, of a name | 4 of 4 in 3 to 4 ms; 3 of 3 in about 50 ms |
|
||||||
|
| `ping 9.9.9.9 2 1392`, then `1393` | Both back; neither back: the tunnel carries 1420 bytes exactly |
|
||||||
|
| `nslookup` | The address, the server and the time; an alias followed; with another server; "there is no nope.invalid" |
|
||||||
|
| `port` | `open, 52 ms`; `refused`; "no answer in 5 s"; "doesn't resolve" |
|
||||||
|
| `traceroute 9.9.9.9` | Nine hops, the tunnel's server first, "arrived" |
|
||||||
|
| A second command while a ping runs | "another one is running: `cancel` stops it" |
|
||||||
|
| `cancel` | "stopped", with the count so far |
|
||||||
|
| In the Shell | Tab completes them; the lines appear there and only there |
|
||||||
|
|
||||||
|
**Not checked:** without the VPN (every check went through the tunnel, or to the local network); a network that drops ICMP; the commands in Safe Mode, where they are not offered.
|
||||||
|
|
||||||
|
**Found on the way:** a refused connection is reported by lwIP as "reset", not "refused"; the first version called it "no route". And the header for the tested half was first given the same name as the service's, which makes a file include itself: the same mistake as an hour before, in the same way.
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ In the Storage App, press <kbd>w</kbd>: the device serves a small web page to an
|
|||||||
|
|
||||||
<kbd>Fn</kbd> + <kbd>p</kbd>, on any screen. The picture goes to `/screenshots` on the SD card. See [Take a screenshot](/howto/screenshot/).
|
<kbd>Fn</kbd> + <kbd>p</kbd>, on any screen. The picture goes to `/screenshots` on the SD card. See [Take a screenshot](/howto/screenshot/).
|
||||||
|
|
||||||
|
## The network doesn't work: how do I find out why?
|
||||||
|
|
||||||
|
From the device itself, in the Shell: `ifconfig`, `ping`, `nslookup`, `port` and `traceroute`. [When the network doesn't work](/howto/network-check/) puts them in order.
|
||||||
|
|
||||||
## Do I need an SD card?
|
## Do I need an SD card?
|
||||||
|
|
||||||
For the radio, GNSS position, Wi-Fi tools, IRC chat and Gemini browsing, no. For anything that is *kept*, yes: notes, IRC logs, Wi-Fi scan logs, GNSS Tracks, LoRa captures, saved Gemini pages and update files. See [Find your files on the SD card](/howto/sd-files/).
|
For the radio, GNSS position, Wi-Fi tools, IRC chat and Gemini browsing, no. For anything that is *kept*, yes: notes, IRC logs, Wi-Fi scan logs, GNSS Tracks, LoRa captures, saved Gemini pages and update files. See [Find your files on the SD card](/howto/sd-files/).
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ Type an App's name **with a capital letter** to open it, without going back to t
|
|||||||
|
|
||||||
The capital is the difference: every command is in small letters, every App starts with a capital. <kbd>Tab</kbd> completes them too.
|
The capital is the difference: every command is in small letters, every App starts with a capital. <kbd>Tab</kbd> completes them too.
|
||||||
|
|
||||||
|
## The network
|
||||||
|
|
||||||
|
For the day the network doesn't do what it should. Each of the first four takes a moment and prints as it goes; one runs at a time, and `cancel` stops it.
|
||||||
|
|
||||||
|
| Command | Tells you |
|
||||||
|
|---|---|
|
||||||
|
| `ping 10.9.0.1` | Whether a host answers, and how fast. A count and a size may follow: `ping 10.9.0.1 10 1392` |
|
||||||
|
| `nslookup roro9stack.net` | A name's addresses, which DNS server answered and in how long. Another server may follow: `nslookup roro9stack.net 9.9.9.9` |
|
||||||
|
| `port git.twis.la 443` | Whether a TCP port is **open**, **refused** (the host is there, nothing listens) or silent (down, or filtered) |
|
||||||
|
| `traceroute 9.9.9.9` | The routers on the way, one a line |
|
||||||
|
| `ifconfig` | The interfaces (Wi-Fi, and the [VPN](/guide/vpn/) when it is up), their addresses, **which one is the default route**, and the DNS servers |
|
||||||
|
| `arp` | The neighbours heard on the Wi-Fi: is the gateway there at all |
|
||||||
|
|
||||||
|
A host can be a name or an address. [When the network doesn't work](/howto/network-check/) puts them in order.
|
||||||
|
|
||||||
## Deleting
|
## Deleting
|
||||||
|
|
||||||
`rm` works as it does on Unix, with one addition: it asks.
|
`rm` works as it does on Unix, with one addition: it asks.
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ vpn auto on|off start with Wi-Fi
|
|||||||
|
|
||||||
`vpn up` with a number of seconds is for trying a new configuration from a distance: if it cuts you off, it comes back by itself.
|
`vpn up` with a number of seconds is for trying a new configuration from a distance: if it cuts you off, it comes back by itself.
|
||||||
|
|
||||||
|
To see it work: `ifconfig` shows the tunnel and whether it is the default route, and `ping`, `nslookup`, `port` and `traceroute` go the way real traffic goes. See [When the network doesn't work](/howto/network-check/).
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
One tunnel, to one server. IPv4 only: IPv6 addresses in the file are left out. The server can be an address or a name.
|
One tunnel, to one server. IPv4 only: IPv6 addresses in the file are left out. The server can be an address or a name.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
+++
|
+++
|
||||||
title = "How-tos"
|
title = "How-tos"
|
||||||
description = "Short recipes for things you will want to do: move files with your phone, set up the VPN, take a screenshot, record a track, capture radio packets, and what to try when something does not work."
|
description = "Short recipes for things you will want to do: move files with your phone, set up the VPN, take a screenshot, find out why the network doesn't work, record a track, capture radio packets, and what to try when something does not work."
|
||||||
template = "guide-index.html"
|
template = "guide-index.html"
|
||||||
page_template = "guide-page.html"
|
page_template = "guide-page.html"
|
||||||
sort_by = "weight"
|
sort_by = "weight"
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
+++
|
||||||
|
title = "When the network doesn't work"
|
||||||
|
description = "Find out where it stops, from the device itself: the Wi-Fi, the gateway, the names, the port, the route, the tunnel."
|
||||||
|
weight = 12
|
||||||
|
[extra]
|
||||||
|
tag = "Network"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Open the [Shell](/guide/shell/) and go down this list. Each step says what a good answer looks like; the first bad one is where the trouble is.
|
||||||
|
|
||||||
|
1. **`ifconfig`**: is there an address, and where does traffic go?
|
||||||
|
|
||||||
|
```
|
||||||
|
ifconfig: vpn 10.9.0.2/32 mtu 1420, up, default route
|
||||||
|
ifconfig: wifi 172.16.42.25/21 gw 172.16.42.1 mtu 1500, up
|
||||||
|
ifconfig: dns 10.9.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
No `wifi` line, or `down`: it is the Wi-Fi itself ([Settings](/guide/settings/#wi-fi)). "default route" says which way everything leaves: on `vpn`, everything goes through the tunnel.
|
||||||
|
|
||||||
|
2. **`ping` the gateway** (the `gw` address): is the local network there?
|
||||||
|
|
||||||
|
```
|
||||||
|
ping: 4/4 back, 0% lost, 3/3/4 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing back: `arp` shows whether the gateway was ever heard.
|
||||||
|
|
||||||
|
3. **`ping 9.9.9.9`**: is the internet there, without names? If the gateway answers and this doesn't, the trouble is beyond your network, or in the tunnel.
|
||||||
|
|
||||||
|
4. **`nslookup roro9stack.net`**: do names resolve?
|
||||||
|
|
||||||
|
```
|
||||||
|
nslookup: 10.9.0.1 answered in 6 ms
|
||||||
|
nslookup: 65.21.233.110
|
||||||
|
```
|
||||||
|
|
||||||
|
"no answer from": that DNS server is unreachable. Try another to compare: `nslookup roro9stack.net 9.9.9.9`. If that one answers, change the servers ([DNS and NTP](/guide/settings/#dns-and-ntp)).
|
||||||
|
|
||||||
|
5. **`port <host> <port>`**: is the service there? `open` is good. `refused` means the machine is up and nothing listens on that port. "no answer" means it is down, or a firewall drops it.
|
||||||
|
|
||||||
|
6. **`traceroute <host>`**: where does it stop? The last router that answers is the last one that works.
|
||||||
|
|
||||||
|
## With the VPN up
|
||||||
|
|
||||||
|
- `ifconfig` shows the `vpn` line, and "default route" on it if everything goes through.
|
||||||
|
- `ping` the server's tunnel address first: that is the tunnel itself.
|
||||||
|
- **Finding the tunnel's real packet size:** `ping 9.9.9.9 2 1392` (1392 bytes, plus 28 of headers, is 1420: WireGuard's usual). If that comes back and 1393 doesn't, the tunnel carries 1420. If 1392 doesn't come back either, try smaller: something on the way allows less, and the server's configuration wants a lower `MTU`.
|
||||||
|
|
||||||
|
## Good to know
|
||||||
|
|
||||||
|
- `cancel` stops a `ping` or a `traceroute`.
|
||||||
|
- Some hosts and routers don't answer pings or traceroutes at all; a silent hop (`*`) in the middle of a route that goes on is normal.
|
||||||
|
- The same commands work over the [Debug Console](/dev/debug/).
|
||||||
@@ -18,7 +18,7 @@ You need a WireGuard server, and a **client configuration** made on it for the C
|
|||||||
|
|
||||||
## Check it
|
## Check it
|
||||||
|
|
||||||
From another machine on the VPN, ping the Cardputer's tunnel address (the `Address` line of the file). Or on the device, in the [Shell](/guide/shell/): `vpn status`.
|
On the device, in the [Shell](/guide/shell/): `vpn status`, then `ifconfig` (a `vpn` line, up) and `ping` the server's tunnel address. Or from another machine on the VPN, ping the Cardputer's tunnel address (the `Address` line of the file). More in [When the network doesn't work](/howto/network-check/).
|
||||||
|
|
||||||
## If it stays dim
|
## If it stays dim
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "apps/demo_app.h"
|
#include "apps/demo_app.h"
|
||||||
#include "apps/note_editor.h"
|
#include "apps/note_editor.h"
|
||||||
#include "apps/shell_app.h"
|
#include "apps/shell_app.h"
|
||||||
|
#include "services/net_tools.h"
|
||||||
#include "services/vpn_service.h"
|
#include "services/vpn_service.h"
|
||||||
#include "services/web_share.h"
|
#include "services/web_share.h"
|
||||||
#include "apps/gemini_app.h"
|
#include "apps/gemini_app.h"
|
||||||
@@ -89,6 +90,7 @@ static IrcService* irc;
|
|||||||
static UpdateService* update;
|
static UpdateService* update;
|
||||||
static DebugConsole* debugConsole;
|
static DebugConsole* debugConsole;
|
||||||
static VpnService* vpnService; // not in Safe Mode
|
static VpnService* vpnService; // not in Safe Mode
|
||||||
|
static NetTools netTools; // ping, nslookup and the rest (issue #90)
|
||||||
static Notifier* notifier;
|
static Notifier* notifier;
|
||||||
static LauncherApp launcher;
|
static LauncherApp launcher;
|
||||||
static AppManager* apps;
|
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"
|
"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"
|
"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"
|
"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"
|
"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 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"
|
"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");
|
else console.println("Not now: Setup is running");
|
||||||
return;
|
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 == "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.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||||
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
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
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ipv4.h"
|
||||||
|
#include "net_probe.h"
|
||||||
|
|
||||||
|
using namespace roro::net;
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
void test_what_was_typed() {
|
||||||
|
PingArgs p;
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parsePing("example.org", p).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("example.org", p.host.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_INT(4, p.count);
|
||||||
|
TEST_ASSERT_EQUAL_INT(56, p.size);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parsePing(" 10.9.0.1 10 1372 ", p).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("10.9.0.1", p.host.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_INT(10, p.count);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1372, p.size);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ping <host> [count] [size]", parsePing("", p).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("a count from 1 to 100", parsePing("a.example 0", p).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("a count from 1 to 100", parsePing("a.example lots", p).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("a size from 0 to 1400 bytes", parsePing("a.example 4 9000", p).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_INT(10, p.count); // a refused line changes nothing
|
||||||
|
|
||||||
|
PortArgs t;
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parsePort("irc.libera.chat 6697", t).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("irc.libera.chat", t.host.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(6697, t.port);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parsePort("10.9.0.1:2323", t).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(2323, t.port);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("port <host> <port>", parsePort("10.9.0.1", t).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("a port from 1 to 65535", parsePort("10.9.0.1 70000", t).c_str());
|
||||||
|
|
||||||
|
LookupArgs l;
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parseLookup("roro9stack.net", l).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", l.server.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parseLookup("roro9stack.net 9.9.9.9", l).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("9.9.9.9", l.server.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("the server as an address: 9.9.9.9", parseLookup("roro9stack.net dns.quad9.net", l).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_an_echo_request_and_its_answers() {
|
||||||
|
uint8_t echo[64];
|
||||||
|
size_t len = buildEcho(echo, sizeof echo, 0xBEEF, 7, 32);
|
||||||
|
TEST_ASSERT_EQUAL_size_t(40, len);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(8, echo[0]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(0, inetChecksum(echo, len)); // a packet with its checksum in sums to nothing
|
||||||
|
TEST_ASSERT_EQUAL_size_t(0, buildEcho(echo, 20, 1, 1, 32));
|
||||||
|
|
||||||
|
// The reply, as a raw socket hands it: an IP header, then the same with type 0.
|
||||||
|
uint8_t reply[20 + 40] = {0x45, 0, 0, 60, 0, 0, 0, 0, 64, 1};
|
||||||
|
std::copy(echo, echo + len, reply + 20);
|
||||||
|
reply[20] = 0;
|
||||||
|
IcmpAnswer a = parseIcmp(reply, sizeof reply);
|
||||||
|
TEST_ASSERT_TRUE(a.kind == IcmpAnswer::Kind::Echo);
|
||||||
|
TEST_ASSERT_EQUAL_HEX16(0xBEEF, a.id);
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(7, a.seq);
|
||||||
|
|
||||||
|
// A router on the way: "time exceeded", with the start of our packet inside it.
|
||||||
|
uint8_t exceeded[20 + 8 + 20 + 8] = {0x45, 0, 0, 56, 0, 0, 0, 0, 250, 1};
|
||||||
|
exceeded[20] = 11;
|
||||||
|
uint8_t* inner = exceeded + 28;
|
||||||
|
inner[0] = 0x45;
|
||||||
|
inner[9] = 1;
|
||||||
|
std::copy(echo, echo + 8, inner + 20);
|
||||||
|
a = parseIcmp(exceeded, sizeof exceeded);
|
||||||
|
TEST_ASSERT_TRUE(a.kind == IcmpAnswer::Kind::TimeExceeded);
|
||||||
|
TEST_ASSERT_EQUAL_HEX16(0xBEEF, a.id);
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(7, a.seq);
|
||||||
|
exceeded[20] = 3;
|
||||||
|
TEST_ASSERT_TRUE(parseIcmp(exceeded, sizeof exceeded).kind == IcmpAnswer::Kind::Unreachable);
|
||||||
|
|
||||||
|
// Not ours to read: someone else's ping to us, a cut packet, UDP.
|
||||||
|
TEST_ASSERT_TRUE(parseIcmp(reply, 22).kind == IcmpAnswer::Kind::Other);
|
||||||
|
reply[20] = 8;
|
||||||
|
TEST_ASSERT_TRUE(parseIcmp(reply, sizeof reply).kind == IcmpAnswer::Kind::Other);
|
||||||
|
reply[20] = 0;
|
||||||
|
reply[9] = 17;
|
||||||
|
TEST_ASSERT_TRUE(parseIcmp(reply, sizeof reply).kind == IcmpAnswer::Kind::Other);
|
||||||
|
TEST_ASSERT_TRUE(parseIcmp(exceeded, 40).kind == IcmpAnswer::Kind::Other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_summary() {
|
||||||
|
PingStats s;
|
||||||
|
s.sent = 4;
|
||||||
|
TEST_ASSERT_EQUAL_STRING("0/4 back, 100% lost", s.summary().c_str());
|
||||||
|
s.add(23);
|
||||||
|
s.add(12);
|
||||||
|
s.add(41);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("3/4 back, 25% lost, 12/25/41 ms", s.summary().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_a_dns_query() {
|
||||||
|
uint8_t q[64];
|
||||||
|
size_t len = buildDnsQuery(q, sizeof q, 0x1234, "roro9stack.net");
|
||||||
|
const uint8_t want[] = {0x12, 0x34, 0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0, 10, 'r', 'o', 'r', 'o', '9', 's', 't', 'a', 'c', 'k', 3, 'n', 'e', 't', 0, 0, 1, 0, 1};
|
||||||
|
TEST_ASSERT_EQUAL_size_t(sizeof want, len);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(want, q, sizeof want);
|
||||||
|
TEST_ASSERT_EQUAL_size_t(len, buildDnsQuery(q, sizeof q, 0x1234, "roro9stack.net.")); // a final dot is the same name
|
||||||
|
TEST_ASSERT_EQUAL_size_t(0, buildDnsQuery(q, sizeof q, 1, ""));
|
||||||
|
TEST_ASSERT_EQUAL_size_t(0, buildDnsQuery(q, sizeof q, 1, "a..b"));
|
||||||
|
TEST_ASSERT_EQUAL_size_t(0, buildDnsQuery(q, sizeof q, 1, std::string(64, 'a') + ".net"));
|
||||||
|
TEST_ASSERT_EQUAL_size_t(0, buildDnsQuery(q, 20, 1, "roro9stack.net"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_a_dns_answer() {
|
||||||
|
// www.example.org is an alias of example.org, which has two addresses. Names after the first
|
||||||
|
// are pointers back into the message, as servers send them.
|
||||||
|
const uint8_t msg[] = {
|
||||||
|
0x12, 0x34, 0x81, 0x80, 0, 1, 0, 3, 0, 0, 0, 0,
|
||||||
|
3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0, 0, 1, 0, 1, // the question, at 12
|
||||||
|
0xC0, 12, 0, 5, 0, 1, 0, 0, 1, 0, 0, 2, 0xC0, 16, // CNAME -> example.org (pointer to 16)
|
||||||
|
0xC0, 16, 0, 1, 0, 1, 0, 0, 1, 0, 0, 4, 93, 184, 216, 34,
|
||||||
|
0xC0, 16, 0, 1, 0, 1, 0, 0, 1, 0, 0, 4, 93, 184, 216, 35,
|
||||||
|
};
|
||||||
|
DnsAnswer a;
|
||||||
|
TEST_ASSERT_TRUE(parseDnsAnswer(msg, sizeof msg, 0x1234, a));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, a.rcode);
|
||||||
|
TEST_ASSERT_FALSE(a.truncated);
|
||||||
|
TEST_ASSERT_EQUAL_size_t(2, a.addresses.size());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("93.184.216.34", formatIpv4(a.addresses[0]).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("93.184.216.35", formatIpv4(a.addresses[1]).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("example.org", a.alias.c_str());
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(parseDnsAnswer(msg, sizeof msg, 0x9999, a)); // someone else's answer
|
||||||
|
for (size_t cut = 0; cut < sizeof msg; cut++) parseDnsAnswer(msg, cut, 0x1234, a); // cut anywhere: no crash
|
||||||
|
TEST_ASSERT_FALSE(parseDnsAnswer(msg, sizeof msg - 3, 0x1234, a));
|
||||||
|
|
||||||
|
uint8_t none[sizeof msg];
|
||||||
|
std::copy(msg, msg + sizeof msg, none);
|
||||||
|
none[3] = 0x83; // no such name
|
||||||
|
none[7] = 0;
|
||||||
|
TEST_ASSERT_TRUE(parseDnsAnswer(none, 33, 0x1234, a));
|
||||||
|
TEST_ASSERT_EQUAL_INT(3, a.rcode);
|
||||||
|
TEST_ASSERT_EQUAL_size_t(0, a.addresses.size());
|
||||||
|
|
||||||
|
// A pointer that points at itself must not hang the reader.
|
||||||
|
uint8_t loop[] = {0x12, 0x34, 0x81, 0x80, 0, 1, 0, 0, 0, 0, 0, 0, 0xC0, 12, 0, 1, 0, 1};
|
||||||
|
TEST_ASSERT_FALSE(parseDnsAnswer(loop, sizeof loop, 0x1234, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_what_was_typed);
|
||||||
|
RUN_TEST(test_an_echo_request_and_its_answers);
|
||||||
|
RUN_TEST(test_the_summary);
|
||||||
|
RUN_TEST(test_a_dns_query);
|
||||||
|
RUN_TEST(test_a_dns_answer);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user