Files
MeshCore/test/fixtures/network_dns.cpp

328 lines
15 KiB
C++

// The marked DNS methods and shared base are copied verbatim from production.
// This fixture replaces only SDK dispatch, address conversion, and link I/O.
#include <helpers/NetworkLink.h>
#include <array>
#include <atomic>
#include <cassert>
#include <climits>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
static void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
constexpr int DNS_MAX_SERVERS = 3;
struct ip_addr_t {
std::array<char, 48> text{};
unsigned type = 0;
bool operator==(const ip_addr_t& other) const {
return type == other.type && text == other.text;
}
};
static ip_addr_t address(const char* text, unsigned type = 4) {
ip_addr_t value;
if (*text) {
require(strlen(text) < value.text.size(), "fixture address too long");
strcpy(value.text.data(), text);
value.type = type;
}
return value;
}
static const ip_addr_t empty_address;
#define IP_ADDR_ANY (&empty_address)
static bool ip_addr_isany_val(const ip_addr_t& value) { return !value.text[0]; }
static char* ipaddr_ntoa_r(const ip_addr_t* value, char* out, int size) {
if (size <= 0) return nullptr;
const size_t length = strlen(value->text.data());
// Match lwIP's partial-write behavior: a short buffer can receive address
// bytes without a terminating NUL before conversion reports failure.
const size_t written = length < size_t(size) ? length + 1 : size_t(size);
memcpy(out, value->text.data(), written);
if (length >= size_t(size)) return nullptr;
return out;
}
using esp_err_t = int;
constexpr esp_err_t ESP_OK = 0;
static bool on_tcpip_thread = false;
static unsigned tcpip_dispatches = 0;
static std::array<ip_addr_t, DNS_MAX_SERVERS> global_dns{};
static std::array<bool, DNS_MAX_SERVERS> null_dns{};
static std::vector<int> dns_reads, dns_writes;
static esp_err_t esp_netif_tcpip_exec(esp_err_t (*callback)(void*), void* context) {
require(!on_tcpip_thread, "nested TCP/IP dispatch");
++tcpip_dispatches;
on_tcpip_thread = true;
const esp_err_t result = callback(context);
on_tcpip_thread = false;
return result;
}
static const ip_addr_t* dns_getserver(int index) {
require(on_tcpip_thread, "DNS read escaped TCP/IP thread");
dns_reads.push_back(index);
return null_dns.at(index) ? nullptr : &global_dns.at(index);
}
static void dns_setserver(int index, const ip_addr_t* value) {
require(on_tcpip_thread, "DNS write escaped TCP/IP thread");
dns_writes.push_back(index);
global_dns.at(index) = *value;
}
struct LinkEndpoint {
IPAddress gateway;
IPAddress gatewayIP() const { return gateway; }
} WiFi, CH390;
@HELPERS@
@BASE@
// Unchanged interface operations are irrelevant to DNS and have no SDK effects.
struct TestLink : NetworkLinkBase {
const char* mediumName() const override { return "test"; }
NetworkMedium medium() const override { return NetworkMedium::None; }
const char* statusName() const override { return "test"; }
int statusCode() const override { return 0; }
bool configValid(const char*) const override { return true; }
void setHostname(const char*) override {}
bool begin(const char*, const char*) override { return false; }
NetworkTransition maintain(uint32_t, uint8_t) override { return NetworkTransition::None; }
bool isConnected() const override { return false; }
IPAddress localIP() const override { return IPAddress(); }
int rssi() const override { return INT_MIN; }
bool resolveHost(const char*, IPAddress&) const override { return false; }
void formatDiagnostics(char*, size_t) const override {}
};
struct WiFiNetworkLink : TestLink { @WiFiNetworkLink@ };
struct EthernetNetworkLink : TestLink { @EthernetNetworkLink@ };
struct AutomaticNetworkLink : TestLink {
std::atomic<NetworkMedium> _selected{NetworkMedium::None};
WiFiNetworkLink _wifi;
EthernetNetworkLink _ethernet;
@AutomaticNetworkLink@
};
static AutomaticNetworkLink automatic;
static unsigned link_lookups = 0;
NetworkLink& activeNetworkLink() { ++link_lookups; return automatic; }
static bool get(const char* config, char* reply) {
@GETTER@ else { return false; }
return true;
}
static void restore_scenario() {
WiFiNetworkLink one, all;
const std::array<ip_addr_t, 3> complete{{
address("1.1.1.1"), address("8.8.8.8"), address("2001:db8::53", 6)}};
const std::array<ip_addr_t, 3> single{{address("9.9.9.9"), {}, {}}};
global_dns = complete;
all.snapshotDns();
global_dns = single;
one.snapshotDns();
dns_writes.clear();
all.restoreDns();
require(global_dns == complete, "DNS restore lost a populated third slot or typed address");
require(dns_writes == std::vector<int>({0, 1, 2}), "DNS restore did not write every slot");
dns_writes.clear();
one.restoreDns();
require(global_dns == single, "DNS restore retained another medium's empty-slot fallbacks");
require(dns_writes == std::vector<int>({0, 1, 2}), "DNS restore omitted empty slots");
require(tcpip_dispatches == 4, "DNS snapshots/restores bypassed thread dispatch");
}
static void never_leased_scenario() {
WiFiNetworkLink never;
const std::array<ip_addr_t, 3> complete{{address("1.1.1.1"), address("8.8.8.8"), address("9.9.9.9")}};
global_dns = complete;
never.restoreDns();
require(global_dns == complete && dns_writes.empty(), "never-leased medium wiped live DNS");
char lease[64] = {};
never.formatLeaseDns(lease, sizeof(lease));
require(std::string(lease) == "none", "never-leased DNS diagnostic is misleading");
global_dns = {};
never.snapshotDns();
global_dns = complete;
never.restoreDns();
require(global_dns == complete && dns_writes.empty(), "resolver-free lease wiped live DNS");
never.formatLeaseDns(lease, sizeof(lease));
require(std::string(lease) == "none", "resolver-free lease invented captured DNS");
}
static void renewal_scenario() {
WiFiNetworkLink link;
const std::array<ip_addr_t, 3> third_only{{{}, {}, address("9.9.9.9")}};
const std::array<ip_addr_t, 3> complete{{address("1.1.1.1"), address("8.8.8.8"), address("9.9.9.9")}};
const std::array<ip_addr_t, 3> partial{{{}, address("10.10.10.10"), {}}};
global_dns = third_only;
link.snapshotDns();
char lease[64] = {};
link.formatLeaseDns(lease, sizeof(lease));
require(std::string(lease) == "-,-,9.9.9.9", "third-slot-only lease was not captured");
global_dns = complete;
link.restoreDns();
require(global_dns == third_only, "third-slot-only DNS restore lost its resolver");
global_dns = complete;
link.snapshotDns();
global_dns = partial;
link.snapshotDns();
global_dns = complete;
link.restoreDns();
require(global_dns == partial, "valid DNS renewal retained obsolete resolver slots");
global_dns = {};
link.snapshotDns();
global_dns = complete;
link.restoreDns();
require(global_dns == partial, "DNS-free renewal wiped the last valid lease");
link.formatLeaseDns(lease, sizeof(lease));
require(std::string(lease) == "-,10.10.10.10", "DNS-free renewal diagnostic lost the last valid lease");
}
static void prepare_automatic() {
WiFi.gateway = IPAddress("10.0.0.1");
CH390.gateway = IPAddress("192.168.1.1");
global_dns = {{address("1.1.1.1"), {}, {}}};
automatic._wifi.snapshotDns();
global_dns = {{address("8.8.8.8"), {}, address("9.9.9.9")}};
automatic._ethernet.snapshotDns();
global_dns = {{address("4.4.4.4"), {}, {}}};
automatic._selected = NetworkMedium::Ethernet;
}
static void format_scenario() {
char list[64] = {};
std::array<ip_addr_t, 3> servers{};
formatDnsList(list, sizeof(list), servers.data(), 3);
require(std::string(list) == "-", "empty resolver list differs from documented sentinel");
servers = {{address("1.1.1.1"), {}, {}}};
formatDnsList(list, sizeof(list), servers.data(), 3);
require(std::string(list) == "1.1.1.1", "trailing empty resolver slots were retained");
servers[2] = address("9.9.9.9");
formatDnsList(list, sizeof(list), servers.data(), 3);
require(std::string(list) == "1.1.1.1,-,9.9.9.9", "interior empty resolver slot was dropped");
global_dns = servers;
null_dns[0] = true;
formatCurrentDnsServers(list, sizeof(list));
require(std::string(list) == "-,-,9.9.9.9", "null lwIP resolver was not treated as empty");
null_dns = {};
prepare_automatic();
const size_t writes_before = dns_writes.size();
char reply[256] = {};
const NetworkLink& wifi = automatic._wifi;
wifi.formatDns(reply, sizeof(reply));
require(std::string(reply) == "> dns:4.4.4.4 gw:10.0.0.1 lease:1.1.1.1", "WiFi DNS interface lost its own lease/gateway");
const NetworkLink& ethernet = automatic._ethernet;
ethernet.formatDns(reply, sizeof(reply));
require(std::string(reply) == "> dns:4.4.4.4 gw:192.168.1.1 lease:8.8.8.8,-,9.9.9.9", "Ethernet DNS interface lost its own lease/gateway");
const NetworkLink& selected = automatic;
selected.formatDns(reply, sizeof(reply));
require(std::string(reply) == "> dns:4.4.4.4 selected:ethernet gw:192.168.1.1\neth-lease:8.8.8.8,-,9.9.9.9 wifi-lease:1.1.1.1", "automatic DNS omitted global or medium lease data");
automatic._selected = NetworkMedium::WiFi;
selected.formatDns(reply, sizeof(reply));
require(strstr(reply, "selected:wifi gw:10.0.0.1"), "automatic WiFi selection used the Ethernet gateway");
automatic._selected = NetworkMedium::None;
selected.formatDns(reply, sizeof(reply));
require(strstr(reply, "selected:none gw:0.0.0.0"), "unselected diagnostic invented a gateway");
// All three lists can exceed the real CLI's 160-byte buffer. Truncation is
// allowed; writing beyond the declared capacity or losing NUL is not.
global_dns = {{address("255.255.255.255"), address("255.255.255.255"), address("255.255.255.255")}};
automatic._wifi.snapshotDns();
automatic._ethernet.snapshotDns();
automatic._selected = NetworkMedium::Ethernet;
CH390.gateway = IPAddress("255.255.255.255");
selected.formatDns(reply, sizeof(reply));
require(strlen(reply) == 207, "long DNS diagnostic fixture no longer exercises truncation");
const std::string full(reply);
for (size_t size : {size_t(0), size_t(1), size_t(2), size_t(10), size_t(64), size_t(160)}) {
std::array<unsigned char, 192> bounded;
bounded.fill(0xA5);
char* out = reinterpret_cast<char*>(bounded.data() + 8);
selected.formatDns(out, size);
for (size_t i = 0; i < 8; ++i) require(bounded[i] == 0xA5, "DNS formatter wrote before destination");
for (size_t i = 8 + size; i < bounded.size(); ++i) require(bounded[i] == 0xA5, "DNS formatter overran declared capacity");
if (size) require(std::string(out) == full.substr(0, size - 1), "bounded DNS reply lost NUL or changed prefix");
}
require(dns_writes.size() == writes_before, "read-only DNS diagnostics changed resolver state");
}
static void ipv6_scenario() {
const char* longest = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
global_dns = {{address(longest, 6), address(longest, 6), address(longest, 6)}};
const std::string full = std::string(longest) + "," + longest + "," + longest;
for (size_t size = 1; size <= 128; ++size) {
std::array<unsigned char, 160> bounded;
bounded.fill(0xA5);
char* out = reinterpret_cast<char*>(bounded.data() + 8);
formatDnsList(out, size, global_dns.data(), 3);
for (size_t i = 0; i < 8; ++i) require(bounded[i] == 0xA5, "IPv6 DNS list wrote before destination");
for (size_t i = 8 + size; i < bounded.size(); ++i) require(bounded[i] == 0xA5, "IPv6 DNS list exceeded capacity");
require(memchr(out, '\0', size) != nullptr, "IPv6 DNS list lost terminator");
require(std::string(out) == full.substr(0, size - 1), "IPv6 DNS list changed truncated prefix");
}
automatic._wifi.snapshotDns();
automatic._ethernet.snapshotDns();
automatic._selected = NetworkMedium::Ethernet;
for (const NetworkLink* link : {static_cast<NetworkLink*>(&automatic._wifi),
static_cast<NetworkLink*>(&automatic._ethernet),
static_cast<NetworkLink*>(&automatic)}) {
struct Reply { unsigned char before[8]; char text[160]; unsigned char after[8]; } reply;
memset(&reply, 0xA5, sizeof(reply));
link->formatDns(reply.text, sizeof(reply.text));
require(memchr(reply.text, '\0', sizeof(reply.text)) != nullptr, "IPv6 DNS diagnostic lost terminator");
require(strncmp(reply.text, "> dns:", 6) == 0, "IPv6 DNS diagnostic lost live resolver prefix");
for (unsigned char c : reply.before) require(c == 0xA5, "IPv6 DNS diagnostic underflowed reply");
for (unsigned char c : reply.after) require(c == 0xA5, "IPv6 DNS diagnostic overflowed reply");
}
}
static void ipv6_termination_scenario() {
const ip_addr_t server = address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 6);
char out[8];
memset(out, 0xA5, sizeof(out));
formatDnsList(out, sizeof(out), &server, 1);
require(memchr(out, '\0', sizeof(out)) != nullptr, "IPv6 DNS list lost terminator");
}
static void zero_scenario() {
char canary = 'Z';
formatDnsList(&canary, 0, global_dns.data(), 3);
require(canary == 'Z', "zero-size DNS formatter wrote to its destination");
formatDnsList(nullptr, 0, global_dns.data(), 3);
formatDnsList(nullptr, 7, global_dns.data(), 3);
}
static void getter_scenario() {
prepare_automatic();
struct Reply { unsigned char before[8]; char text[160]; unsigned char after[8]; } reply;
memset(&reply, 0xA5, sizeof(reply));
require(get("link.dns", reply.text), "observer DNS getter was not recognized");
require(link_lookups == 1 && strstr(reply.text, "> dns:4.4.4.4 selected:ethernet"), "observer DNS getter did not use the live NetworkLink interface");
for (unsigned char c : reply.before) require(c == 0xA5, "observer DNS getter underflowed reply");
for (unsigned char c : reply.after) require(c == 0xA5, "observer DNS getter overflowed reply");
const std::string original(reply.text);
for (const char* rejected : {"link.dnsX", "LINK.DNS", "link.dns ", "wifi.status"}) {
require(!get(rejected, reply.text), "observer DNS getter accepted another command");
require(std::string(reply.text) == original && link_lookups == 1, "rejected DNS command changed reply or queried the link");
}
}
int main(int argc, char** argv) {
try {
require(argc == 2, "missing DNS scenario");
const std::string scenario(argv[1]);
if (scenario == "restore") restore_scenario();
else if (scenario == "never-leased") never_leased_scenario();
else if (scenario == "renewal") renewal_scenario();
else if (scenario == "format") format_scenario();
else if (scenario == "ipv6") ipv6_scenario();
else if (scenario == "ipv6-termination") ipv6_termination_scenario();
else if (scenario == "zero") zero_scenario();
else if (scenario == "getter") getter_scenario();
else throw std::runtime_error("unknown DNS scenario");
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}