Merge upstream develop into packet authentication policy

This commit is contained in:
Benjamin Faershtein
2026-07-20 11:48:59 -07:00
74 changed files with 836 additions and 939 deletions
+27 -15
View File
@@ -177,6 +177,9 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
}
#endif
if (p->to == NODENUM_BROADCAST_NO_LORA)
return false;
// Allow rebroadcast if hop_limit > 0 OR if we're exhausting hops (which sets hop_limit = 0 but still needs one relay)
if (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) {
if (p->id != 0) {
@@ -210,11 +213,10 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
}
#endif
if (p->next_hop == NO_NEXT_HOP_PREFERENCE) {
FloodingRouter::send(tosend);
} else {
NextHopRouter::send(tosend);
}
ErrorCode res =
(p->next_hop == NO_NEXT_HOP_PREFERENCE) ? FloodingRouter::send(tosend) : NextHopRouter::send(tosend);
if (res == ERRNO_SHOULD_RELEASE)
packetPool.release(tosend);
return true;
}
@@ -419,8 +421,10 @@ int32_t NextHopRouter::doRetransmissions()
trafficManagementModule->clearNextHop(p.packet->to);
}
#endif
if (auto *copy = packetPool.allocCopy(*p.packet))
FloodingRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} else {
#if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED
// M4 (gated): if the route isn't proven healthy, don't spend a second directed
@@ -434,22 +438,30 @@ int32_t NextHopRouter::doRetransmissions()
meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to);
if (sentTo)
sentTo->next_hop = NO_NEXT_HOP_PREFERENCE;
if (auto *copy = packetPool.allocCopy(*p.packet))
FloodingRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} else {
if (auto *copy = packetPool.allocCopy(*p.packet))
NextHopRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
}
#else
if (auto *copy = packetPool.allocCopy(*p.packet))
NextHopRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
#endif
}
} else {
// Note: we call the superclass version because we don't want to have our version of send() add a new
// retransmission record
if (auto *copy = packetPool.allocCopy(*p.packet))
FloodingRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
}
// Queue again
+4
View File
@@ -206,7 +206,11 @@ class NextHopRouter : public FloodingRouter
*/
std::optional<uint8_t> getNextHop(NodeNum to, uint8_t relay_node);
#ifdef PIO_UNIT_TESTING
public: // expose perhapsRebroadcast to the test shim
#else
private:
#endif
/** Check if we should be rebroadcasting this packet if so, do so.
* @return true if we did rebroadcast */
bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override;
+8 -1
View File
@@ -3312,13 +3312,20 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
/** Update user info and channel for this node based on received user data
*/
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex)
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex, bool xeddsaSigned)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
if (!info) {
return false;
}
// Once a node has proven it signs, only a signed update may change its identity. The public-key guard
// below is no help - an attacker can replay the victim's real (public) key. Our own record is exempt.
if (nodeId != getNodeNum() && nodeInfoLiteHasXeddsaSigned(info) && !xeddsaSigned) {
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
return false;
}
#if !(MESHTASTIC_EXCLUDE_PKI)
if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) {
printBytes("Incoming Pubkey: ", p.public_key.bytes, 32);
+3 -3
View File
@@ -255,9 +255,9 @@ class NodeDB
*/
void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO);
/** Update user info and channel for this node based on received user data
*/
bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0);
/** Update user info and channel for this node based on received user data.
* A known signer's identity is only learned when xeddsaSigned; defaults false so callers fail closed. */
bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0, bool xeddsaSigned = false);
/*
* Sets a node either favorite or unfavorite. Returns true if the node ends
+65 -19
View File
@@ -359,15 +359,6 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
return send(p);
}
}
/**
* Send a packet on a suitable interface.
*/
ErrorCode Router::rawSend(meshtastic_MeshPacket *p)
{
assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
return iface->send(p);
}
/**
* Send a packet on a suitable interface. This routine will
* later free() the packet to pool. This routine is not allowed to stall.
@@ -531,6 +522,62 @@ void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Rout
}
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
/** Size a decoded Data as the sender's signedDataFits() gate would have, with padding stripped:
* unknown fields inside Data.payload survive in payload.size and would otherwise let a forger
* inflate an unsigned broadcast past the signable budget. Returns false only if sizing failed.
* Sizing only what this build's schema decodes, so a signable type that later grows needs its
* legitimate maximum re-checked against the budget or honest unsigned broadcasts get dropped. */
static bool canonicalSignableSize(meshtastic_Data *d, size_t *size)
{
const pb_msgdesc_t *fields = nullptr;
switch (d->portnum) {
case meshtastic_PortNum_POSITION_APP:
fields = &meshtastic_Position_msg;
break;
case meshtastic_PortNum_TELEMETRY_APP:
fields = &meshtastic_Telemetry_msg;
break;
case meshtastic_PortNum_WAYPOINT_APP:
fields = &meshtastic_Waypoint_msg;
break;
case meshtastic_PortNum_NODEINFO_APP:
fields = &meshtastic_User_msg;
break;
default:
break;
}
if (fields) {
// Scratch kept off the stack: these decoded structs are large for the smaller MCU targets.
// Safe as file-static state because both callers of checkXeddsaReceivePolicy hold cryptLock.
static union {
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_Position position;
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_Telemetry telemetry;
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_Waypoint waypoint;
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_User user;
} inner;
memset(&inner, 0, sizeof(inner));
size_t canonicalPayload;
if (pb_decode_from_bytes(d->payload.bytes, d->payload.size, fields, &inner) &&
pb_get_encoded_size(&canonicalPayload, fields, &inner) && canonicalPayload <= d->payload.size) {
// Only the length matters when sizing a bytes field, so swap it in place instead of
// copying the whole Data; restored below because modules still need the real payload.
const pb_size_t prevSize = d->payload.size;
d->payload.size = (pb_size_t)canonicalPayload;
const bool sized = pb_get_encoded_size(size, &meshtastic_Data_msg, d);
d->payload.size = prevSize;
return sized;
}
}
return pb_get_encoded_size(size, &meshtastic_Data_msg, d);
}
enum class NodeInfoBootstrapResult { NOT_APPLICABLE, VERIFIED, INVALID };
static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket *p)
@@ -557,7 +604,7 @@ static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket
return NodeInfoBootstrapResult::VERIFIED;
}
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
{
const auto policy = config.security.packet_signature_policy;
const bool strict = policy == meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
@@ -617,15 +664,14 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
return true;
// In Balanced, preserve legacy unsigned-unicast compatibility and only reject a signable
// unsigned broadcast from a known signer. encodedDataSize is
// the size of the encoded Data exactly as the sender built it (or 0 to size p->decoded
// canonically); with no signature field present it is the unsigned base, and adding
// XEDDSA_SIGNATURE_FIELD_BYTES mirrors the sender-side signedDataFits() gate per packet,
// whatever fields the Data carried. Oversized broadcasts remain compatible.
// unsigned broadcast from a known signer. Canonical sizing removes unknown protobuf
// fields before mirroring the sender-side signedDataFits() gate. Oversized broadcasts
// remain compatible.
if (nodeDB->hasSeenXeddsaSigner(p->from) && isBroadcast(p->to)) {
if (encodedDataSize == 0 && !pb_get_encoded_size(&encodedDataSize, &meshtastic_Data_msg, &p->decoded))
size_t canonicalSize;
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
return true; // can't size it; never drop on a sizing failure
if (encodedDataSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
if (canonicalSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
return false;
}
@@ -813,8 +859,8 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
p->channel = chIndex; // change to store the index instead of the hash
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
// Use the encoded Data size before merging local-only bitfield state.
if (!checkXeddsaReceivePolicy(p, rawSize))
// Run before merging local-only bitfield state into the decoded Data.
if (!checkXeddsaReceivePolicy(p))
return DecodeState::DECODE_POLICY_REJECT;
#endif
+3 -4
View File
@@ -94,7 +94,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory
* NOTE: This method will free the provided packet (even if we return an error code)
*/
virtual ErrorCode send(meshtastic_MeshPacket *p);
virtual ErrorCode rawSend(meshtastic_MeshPacket *p);
/* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it
before us */
@@ -187,9 +186,9 @@ void resetRoutingAuthEvaluationCount();
meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
/** Enforce the configured XEdDSA receive policy; zero encodedDataSize derives it canonically.
* The caller must hold cryptLock. Returns false when the packet must be dropped. */
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize = 0);
/** Enforce the configured XEdDSA receive policy. The caller must hold cryptLock.
* Returns false when the packet must be dropped. */
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
#endif
extern Router *router;
-137
View File
@@ -74,15 +74,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio);
ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio);
// ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot);
// ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot);
ResourceNode *nodeAdmin = new ResourceNode("/admin", "GET", &handleAdmin);
// ResourceNode *nodeAdminSettings = new ResourceNode("/admin/settings", "GET", &handleAdminSettings);
// ResourceNode *nodeAdminSettingsApply = new ResourceNode("/admin/settings/apply", "POST", &handleAdminSettingsApply);
// ResourceNode *nodeAdminFs = new ResourceNode("/admin/fs", "GET", &handleFs);
// ResourceNode *nodeUpdateFs = new ResourceNode("/admin/fs/update", "POST", &handleUpdateFs);
// ResourceNode *nodeDeleteFs = new ResourceNode("/admin/fs/delete", "GET", &handleDeleteFsContent);
ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart);
ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
@@ -100,8 +92,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeAPIv1ToRadio);
secureServer->registerNode(nodeAPIv1FromRadioOptions);
secureServer->registerNode(nodeAPIv1FromRadio);
// secureServer->registerNode(nodeHotspotApple);
// secureServer->registerNode(nodeHotspotAndroid);
secureServer->registerNode(nodeRestart);
secureServer->registerNode(nodeFormUpload);
secureServer->registerNode(nodeJsonScanNetworks);
@@ -109,12 +99,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeJsonDelete);
secureServer->registerNode(nodeJsonReport);
secureServer->registerNode(nodeJsonNodes);
// secureServer->registerNode(nodeUpdateFs);
// secureServer->registerNode(nodeDeleteFs);
secureServer->registerNode(nodeAdmin);
// secureServer->registerNode(nodeAdminFs);
// secureServer->registerNode(nodeAdminSettings);
// secureServer->registerNode(nodeAdminSettingsApply);
secureServer->registerNode(nodeRoot); // This has to be last
// Insecure nodes
@@ -122,20 +107,13 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
insecureServer->registerNode(nodeAPIv1ToRadio);
insecureServer->registerNode(nodeAPIv1FromRadioOptions);
insecureServer->registerNode(nodeAPIv1FromRadio);
// insecureServer->registerNode(nodeHotspotApple);
// insecureServer->registerNode(nodeHotspotAndroid);
insecureServer->registerNode(nodeRestart);
insecureServer->registerNode(nodeFormUpload);
insecureServer->registerNode(nodeJsonScanNetworks);
insecureServer->registerNode(nodeJsonFsBrowseStatic);
insecureServer->registerNode(nodeJsonDelete);
insecureServer->registerNode(nodeJsonReport);
// insecureServer->registerNode(nodeUpdateFs);
// insecureServer->registerNode(nodeDeleteFs);
insecureServer->registerNode(nodeAdmin);
// insecureServer->registerNode(nodeAdminFs);
// insecureServer->registerNode(nodeAdminSettings);
// insecureServer->registerNode(nodeAdminSettingsApply);
insecureServer->registerNode(nodeRoot); // This has to be last
}
@@ -230,36 +208,6 @@ void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res)
LOG_DEBUG("webAPI handleAPIv1ToRadio");
}
void htmlDeleteDir(const char *dirname)
{
File root = FSCom.open(dirname);
if (!root) {
return;
}
if (!root.isDirectory()) {
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
htmlDeleteDir(file.name());
file.flush();
file.close();
} else {
String fileName = String(file.name());
file.flush();
file.close();
LOG_DEBUG(" %s", fileName.c_str());
FSCom.remove(fileName);
}
file = root.openNextFile();
}
root.flush();
root.close();
}
// Escape a string into a JSON double-quoted literal. Matches the previous
// SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase,
// escapes " \ / \b \f \n \r \t, UTF-8 passes through unchanged).
@@ -858,45 +806,6 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
res->print(out.c_str());
}
/*
This supports the Apple Captive Network Assistant (CNA) Portal
*/
void handleHotspot(HTTPRequest *req, HTTPResponse *res)
{
LOG_INFO("Hotspot Request");
/*
If we don't do a redirect, be sure to return a "Success" message
otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
*/
// Status code is 200 OK by default.
// We want to deliver a simple HTML page, so we send a corresponding content type:
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
// res->println("<!DOCTYPE html>");
res->println("<meta http-equiv=\"refresh\" content=\"0;url=/\" />");
}
void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("Delete Content in /static/*");
LOG_INFO("Delete files from /static/* : ");
concurrency::LockGuard g(spiLock);
htmlDeleteDir("/static");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleAdmin(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
@@ -909,52 +818,6 @@ void handleAdmin(HTTPRequest *req, HTTPResponse *res)
res->println("<a href=/json/report>Device Report</a><br>");
}
void handleAdminSettings(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("This isn't done.");
res->println("<form action=/admin/settings/apply method=post>");
res->println("<table border=1>");
res->println("<tr><td>Set?</td><td>Setting</td><td>current value</td><td>new value</td></tr>");
res->println("<tr><td><input type=checkbox></td><td>WiFi SSID</td><td>false</td><td><input type=radio></td></tr>");
res->println("<tr><td><input type=checkbox></td><td>WiFi Password</td><td>false</td><td><input type=radio></td></tr>");
res->println(
"<tr><td><input type=checkbox></td><td>Smart Position Update</td><td>false</td><td><input type=radio></td></tr>");
res->println("</table>");
res->println("<table>");
res->println("<input type=submit value=Apply New Settings>");
res->println("<form>");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "POST");
res->println("<h1>Meshtastic</h1>");
res->println(
"<html><head><meta http-equiv=\"refresh\" content=\"1;url=/admin/settings\" /><title>Settings Applied. </title>");
res->println("Settings Applied. Please wait.");
}
void handleFs(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("<a href=/admin/fs/delete>Delete Web Content</a><p><form action=/admin/fs/update "
"method=post><input type=submit value=UPDATE_WEB_CONTENT></form>Be patient!");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleRestart(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
-6
View File
@@ -4,7 +4,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer);
// Declare some handler functions for the various URLs on the server
void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res);
void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res);
void handleHotspot(HTTPRequest *req, HTTPResponse *res);
void handleStatic(HTTPRequest *req, HTTPResponse *res);
void handleRestart(HTTPRequest *req, HTTPResponse *res);
void handleFormUpload(HTTPRequest *req, HTTPResponse *res);
@@ -13,12 +12,7 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res);
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res);
void handleReport(HTTPRequest *req, HTTPResponse *res);
void handleNodes(HTTPRequest *req, HTTPResponse *res);
void handleUpdateFs(HTTPRequest *req, HTTPResponse *res);
void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res);
void handleFs(HTTPRequest *req, HTTPResponse *res);
void handleAdmin(HTTPRequest *req, HTTPResponse *res);
void handleAdminSettings(HTTPRequest *req, HTTPResponse *res);
void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res);
// Interface to the PhoneAPI to access the protobufs with messages
class HttpAPI : public PhoneAPI
-11
View File
@@ -1,14 +1,3 @@
#include "mesh/http/ContentHelper.h"
// #include <Arduino.h>
// #include "main.h"
void replaceAll(std::string &str, const std::string &from, const std::string &to)
{
if (from.empty())
return;
size_t start_pos = 0;
while ((start_pos = str.find(from, start_pos)) != std::string::npos) {
str.replace(start_pos, from.length(), to);
start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx'
}
}
-2
View File
@@ -3,5 +3,3 @@
#include <string>
#define BoolToString(x) ((x) ? "true" : "false")
void replaceAll(std::string &str, const std::string &from, const std::string &to);