emdashes begone (#10847)
This commit is contained in:
@@ -443,7 +443,7 @@ bool Channels::usesPublicKey(ChannelIndex chIndex)
|
||||
bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
||||
{
|
||||
const auto &ch = getByIndex(chIndex);
|
||||
// Absent (unencrypted) or single-byte PSK — all the well-known key indexes
|
||||
// Absent (unencrypted) or single-byte PSK - all the well-known key indexes
|
||||
if (ch.settings.psk.size > 1)
|
||||
return false;
|
||||
|
||||
@@ -451,7 +451,7 @@ bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
||||
for (int p = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; p <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; p++) {
|
||||
const char *presetName =
|
||||
DisplayFormatters::getModemPresetDisplayName(static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(p), false, true);
|
||||
// Presets without a display name fall through to "Invalid" — never a match
|
||||
// Presets without a display name fall through to "Invalid" - never a match
|
||||
if (strcmp(presetName, "Invalid") != 0 && strcmp(name, presetName) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t d
|
||||
int8_t throttle =
|
||||
(type == TrafficType::POSITION) ? myRegion->profile->positionThrottle : myRegion->profile->telemetryThrottle;
|
||||
|
||||
// throttle <= 0 means unset; 1 is the neutral multiplier — skip the multiply for performance
|
||||
// throttle <= 0 means unset; 1 is the neutral multiplier - skip the multiply for performance
|
||||
if (throttle <= 1)
|
||||
return baseMs;
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
||||
filled = true;
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
// Browser/wasm: no getrandom/arc4random — fall through to std::random_device,
|
||||
// Browser/wasm: no getrandom/arc4random - fall through to std::random_device,
|
||||
// which emscripten backs with crypto.getRandomValues().
|
||||
#else
|
||||
// arc4random_buf is available on Darwin/BSD and cannot fail.
|
||||
|
||||
@@ -205,7 +205,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
||||
err = lora.setOutputPower(power);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
||||
// Apply RX gain mode - valid in STDBY, matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
@@ -326,7 +326,7 @@ template <typename T> void LR11x0Interface<T>::resetAGC()
|
||||
|
||||
LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
||||
|
||||
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
|
||||
// 1. Warm sleep - powers down the analog frontend, resetting AGC state
|
||||
lora.sleep(true, 0);
|
||||
|
||||
// 2. Wake to RC standby for stable calibration
|
||||
|
||||
@@ -211,7 +211,7 @@ template <typename T> bool LR20x0Interface<T>::reconfigure()
|
||||
err = lora.setOutputPower(power);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
||||
// Apply RX gain mode - valid in STDBY, matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
@@ -332,7 +332,7 @@ template <typename T> void LR20x0Interface<T>::resetAGC()
|
||||
|
||||
LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
||||
|
||||
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
|
||||
// 1. Warm sleep - powers down the analog frontend, resetting AGC state
|
||||
lora.sleep(true, 0);
|
||||
|
||||
// 2. Wake to RC standby for stable calibration
|
||||
|
||||
@@ -100,7 +100,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
|
||||
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
|
||||
// directly from the destination. checkRelayers is read-only on PacketHistory and O(1), so we run it even
|
||||
// when origTx is absent — that lets us still capture the confirmed hop into the TMM overflow cache below.
|
||||
// when origTx is absent - that lets us still capture the confirmed hop into the TMM overflow cache below.
|
||||
// Single lookup for both relayer checks on the same (request_id, to) pair
|
||||
bool wasAlreadyRelayer = false;
|
||||
bool weWereSoleRelayer = false;
|
||||
@@ -110,7 +110,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
|
||||
// M1/M2: only learn a next hop whose last byte maps to a single plausible relay. On a dense
|
||||
// mesh the byte may be ambiguous; storing it would aim future DMs at the wrong node. This gate
|
||||
// now protects BOTH the hot-store route (NodeInfoLite.next_hop) AND the TMM overflow cache —
|
||||
// now protects BOTH the hot-store route (NodeInfoLite.next_hop) AND the TMM overflow cache -
|
||||
// the overflow cache deliberately holds many more next-hop bytes (long-tail nodes), so it is
|
||||
// even more collision-prone and must never store an ambiguous byte either. Ambiguous/unknown
|
||||
// -> store nothing and keep flooding (safe).
|
||||
@@ -162,7 +162,7 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
if (p->id != 0) {
|
||||
if (isRebroadcaster()) {
|
||||
// NOTE: this is a self-identity match (is the addressed next_hop OUR last byte?), so it
|
||||
// cannot be hardened with resolveLastByte() — a remote node that legitimately shares our
|
||||
// cannot be hardened with resolveLastByte() - a remote node that legitimately shares our
|
||||
// last byte will also match here and rebroadcast. That residual collision needs a wider
|
||||
// on-wire field to fix. M1/M2 instead shrink the blast radius by reducing how often an
|
||||
// ambiguous next_hop byte is ever learned (sniffReceived) or originated (getNextHop).
|
||||
@@ -382,7 +382,7 @@ int32_t NextHopRouter::doRetransmissions()
|
||||
if (!isBroadcast(p.packet->to)) {
|
||||
if (p.numRetransmissions == 1) {
|
||||
// Last retransmission: this directed delivery went un-ACKed. Record the failure
|
||||
// (M3 — accumulates across DMs to age out a flapping/dead route) and reset
|
||||
// (M3 - accumulates across DMs to age out a flapping/dead route) and reset
|
||||
// next_hop so the final try falls back to FloodingRouter.
|
||||
noteRouteFailure(p.packet->to);
|
||||
p.packet->next_hop = NO_NEXT_HOP_PREFERENCE;
|
||||
@@ -401,7 +401,7 @@ int32_t NextHopRouter::doRetransmissions()
|
||||
} else {
|
||||
#if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED
|
||||
// M4 (gated): if the route isn't proven healthy, don't spend a second directed
|
||||
// attempt — start flooding one retry sooner to cut recovery latency. A verified
|
||||
// attempt - start flooding one retry sooner to cut recovery latency. A verified
|
||||
// route (fresh, zero recent failures) keeps the unchanged directed-retry path so
|
||||
// the sparse-mesh happy path is untouched.
|
||||
RouteHealth *h = findRouteHealth(p.packet->to);
|
||||
|
||||
+25
-25
@@ -723,7 +723,7 @@ template <typename Map> std::vector<NodeNum> snapshotSatelliteNodeNums(const Map
|
||||
}
|
||||
|
||||
// Drop the stalest entry of `map` (staleness proxied via the owner's
|
||||
// last_heard; 0 = owner evicted, i.e. an orphan — first out). Never evicts our
|
||||
// last_heard; 0 = owner evicted, i.e. an orphan - first out). Never evicts our
|
||||
// own node's entry. Caller holds satelliteMutex. Returns false if nothing
|
||||
// could be evicted.
|
||||
template <typename Map> bool evictStalestSatellite(NodeDB &db, Map &map)
|
||||
@@ -1797,7 +1797,7 @@ bool NodeDB::enforceSatelliteCaps()
|
||||
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Classify an evicted node's hop-protected category for the warm tier. Favorite/ignored/
|
||||
// verified are local flags (rarely reach warm — they're eviction-protected — but classify
|
||||
// verified are local flags (rarely reach warm - they're eviction-protected - but classify
|
||||
// them if they do); otherwise tracker/sensor/tak_tracker are role-protected.
|
||||
static uint8_t warmProtectedCategory(const meshtastic_NodeInfoLite &n)
|
||||
{
|
||||
@@ -1940,7 +1940,7 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
|
||||
// check if the file is encrypted and decrypt before protobuf decode
|
||||
if (EncryptedStorage::isEncrypted(filename)) {
|
||||
// ZeroizingArrayPtr wipes the decrypted plaintext (which contains config
|
||||
// secrets — channel PSKs, security private_key, etc.) before delete[],
|
||||
// secrets - channel PSKs, security private_key, etc.) before delete[],
|
||||
// so it isn't recoverable from the heap after this function returns.
|
||||
auto decBuf = meshtastic_security::make_zeroizing_array(protoSize);
|
||||
if (!decBuf) {
|
||||
@@ -2028,7 +2028,7 @@ void NodeDB::demoteOldestHotNodesToWarm()
|
||||
warmStore.absorb(n.num, n.last_heard, n.public_key.size > 0 ? n.public_key.bytes : nullptr, n.role,
|
||||
warmProtectedCategory(n));
|
||||
// Demotion drops the node from the header table, so drop its satellites
|
||||
// too (the eviction chokepoint) — they'd otherwise orphan until the next
|
||||
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
||||
// enforceSatelliteCaps pass.
|
||||
eraseNodeSatellites(n.num);
|
||||
demoted++;
|
||||
@@ -2079,7 +2079,7 @@ void NodeDB::nodeDBSelfCare()
|
||||
|
||||
const bool satsTrimmed = enforceSatelliteCaps();
|
||||
|
||||
// Ensure self exists, sits at index 0, and carries current owner info — after
|
||||
// Ensure self exists, sits at index 0, and carries current owner info - after
|
||||
// any demotion has freed a slot. Covers the foreign/fixture case where the
|
||||
// loaded file did not contain us at all.
|
||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(self);
|
||||
@@ -2090,7 +2090,7 @@ void NodeDB::nodeDBSelfCare()
|
||||
}
|
||||
|
||||
// One-shot rewrite: only when we healed something, and never while storage
|
||||
// is locked — a locked boot loads placeholder defaults that must not be written
|
||||
// is locked - a locked boot loads placeholder defaults that must not be written
|
||||
// over the encrypted store; reloadFromDisk() re-runs self-care once unlocked.
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
const bool storageLocked = EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked();
|
||||
@@ -2171,7 +2171,7 @@ void NodeDB::loadFromDisk()
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// Only take the locked-boot defaults path when lockdown is ACTIVE (the
|
||||
// device is provisioned) AND storage is still locked. A lockdown-capable
|
||||
// build that has never been provisioned — or that was disabled — falls
|
||||
// build that has never been provisioned - or that was disabled - falls
|
||||
// through to the normal plaintext load below and behaves like stock.
|
||||
if (EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked()) {
|
||||
// Encrypted storage is locked. Install defaults and wait for the
|
||||
@@ -2188,7 +2188,7 @@ void NodeDB::loadFromDisk()
|
||||
// would otherwise honour USERPREFS_CONFIG_LORA_REGION (the common shape
|
||||
// for managed deployments) and the LongFast default channel synthesised
|
||||
// by installDefaultChannels, so the device would beacon nodeinfo /
|
||||
// telemetry on the public default PSK before any unlock — and process
|
||||
// telemetry on the public default PSK before any unlock - and process
|
||||
// incoming default-channel packets the same way. Forcing region=UNSET
|
||||
// gates both TX and RX in RadioLibInterface (see the region==UNSET
|
||||
// checks in startSend and readData); tx_enabled=false is belt-and-
|
||||
@@ -2262,7 +2262,7 @@ void NodeDB::loadFromDisk()
|
||||
|
||||
// Left UNTRIMMED on purpose: trim/demote/satellite-cap/self-pin/rewrite all
|
||||
// run in nodeDBSelfCare() once getNodeNum() is valid (still 0 here on a cold
|
||||
// boot, so we could only assume index 0 == self — the very bug being fixed).
|
||||
// boot, so we could only assume index 0 == self - the very bug being fixed).
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Load the warm tier so its on-disk snapshot is available before the node DB
|
||||
// is exercised (and before nodeDBSelfCare() demotes any overflow into it).
|
||||
@@ -2445,7 +2445,7 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
|
||||
// Always-on traffic management: a device that has NEVER configured TMM
|
||||
// (has_traffic_management false — AdminModule always sets the has_ flag on
|
||||
// (has_traffic_management false - AdminModule always sets the has_ flag on
|
||||
// write, even when disabling) gets the fork defaults. Explicitly configured
|
||||
// devices keep their exact settings.
|
||||
if (!moduleConfig.has_traffic_management) {
|
||||
@@ -2479,7 +2479,7 @@ void NodeDB::loadFromDisk()
|
||||
// without saving to disk, so we force a save here to ensure encrypted files exist.
|
||||
//
|
||||
// Only when lockdown is ACTIVE. A capable-but-off device must leave its
|
||||
// files as plaintext — encryptAndWrite would fail anyway (no DEK), but
|
||||
// files as plaintext - encryptAndWrite would fail anyway (no DEK), but
|
||||
// skipping the whole block avoids the wasted attempts and error logs.
|
||||
if (EncryptedStorage::isLockdownActive()) {
|
||||
const char *filesToCheck[] = {configFileName, moduleConfigFileName, channelFileName, deviceStateFileName,
|
||||
@@ -2548,7 +2548,7 @@ void NodeDB::loadFromDisk()
|
||||
// Serializes reloadFromDisk against itself. Other readers of config /
|
||||
// channelFile / nodeDatabase don't take this lock today, so this only
|
||||
// prevents reload-vs-reload races (e.g. fast successive unlocks). It is
|
||||
// not a full data-race fix for those structs — that would require
|
||||
// not a full data-race fix for those structs - that would require
|
||||
// thread-shared locking discipline across the whole codebase, beyond
|
||||
// the audit's M7 scope. The radio standby+reconfigure below keeps the
|
||||
// radio out of the window where SX12xx registers are mid-swap.
|
||||
@@ -2561,7 +2561,7 @@ static concurrency::Lock g_reloadFromDiskMutex;
|
||||
* reconfigure() to push the now-real settings to the chip.
|
||||
*
|
||||
* Returns true iff every encrypted file decrypted and decoded cleanly.
|
||||
* On false the caller MUST treat storage as corrupt — see header.
|
||||
* On false the caller MUST treat storage as corrupt - see header.
|
||||
*/
|
||||
bool NodeDB::reloadFromDisk()
|
||||
{
|
||||
@@ -2580,7 +2580,7 @@ bool NodeDB::reloadFromDisk()
|
||||
loadFromDisk();
|
||||
|
||||
if (storageCorruptThisLoad) {
|
||||
LOG_ERROR("NodeDB: storage decrypt/decode failed during reload — surfacing as corrupt");
|
||||
LOG_ERROR("NodeDB: storage decrypt/decode failed during reload - surfacing as corrupt");
|
||||
// Leave the radio sleeping. Caller will lock storage and emit
|
||||
// a LOCKED(storage_corrupt) status; we must not reconfigure
|
||||
// the chip with the locked-default placeholder values still
|
||||
@@ -2629,7 +2629,7 @@ bool NodeDB::disableLockdownToPlaintext()
|
||||
}
|
||||
}
|
||||
|
||||
// All files are plaintext now — remove the lockdown artifacts. Deleting
|
||||
// All files are plaintext now - remove the lockdown artifacts. Deleting
|
||||
// /prefs/.dek is the atomic commit: after it, isLockdownActive() is false.
|
||||
EncryptedStorage::removeLockdownArtifacts();
|
||||
return true;
|
||||
@@ -2652,11 +2652,11 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
// Encrypt all files except uiconfig (no secrets) and the DEK file (self-encrypted).
|
||||
// Only when lockdown is ACTIVE (provisioned). A lockdown-capable but DISABLED
|
||||
// device has no DEK, so encryptAndWrite would fail and config would never
|
||||
// persist — it must save plaintext exactly like stock firmware. Once enabled,
|
||||
// persist - it must save plaintext exactly like stock firmware. Once enabled,
|
||||
// the reloadFromDisk migrate pass re-saves these plaintext files encrypted.
|
||||
if (EncryptedStorage::isLockdownActive() && strcmp(filename, uiconfigFileName) != 0) {
|
||||
// ZeroizingArrayPtr wipes the unencrypted protobuf encoding (which contains
|
||||
// config secrets — channel PSKs, security private_key, etc.) before delete[],
|
||||
// config secrets - channel PSKs, security private_key, etc.) before delete[],
|
||||
// so plaintext copies aren't left in heap memory after encryption completes.
|
||||
auto pbBuf = meshtastic_security::make_zeroizing_array(protoSize);
|
||||
if (!pbBuf) {
|
||||
@@ -2853,7 +2853,7 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
// reset the 8s HW watchdog so the second write gets a full budget (issue #10746).
|
||||
watchdog_update();
|
||||
#endif
|
||||
// Same cadence as the node DB; failure is logged but must not propagate —
|
||||
// Same cadence as the node DB; failure is logged but must not propagate -
|
||||
// a false return from here would trigger saveToDisk()'s fsFormat() path.
|
||||
warmStore.saveIfDirty();
|
||||
#endif
|
||||
@@ -2874,14 +2874,14 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
// When lockdown is ACTIVE but storage is still locked, encryptAndWrite()
|
||||
// returns false for every file. That would cause saveToDisk()'s nRF52 retry
|
||||
// path to call FSCom.format(), wiping all encrypted proto files from flash.
|
||||
// Return true here — "nothing to save, not an error."
|
||||
// Return true here - "nothing to save, not an error."
|
||||
//
|
||||
// Gate on isLockdownActive(): a lockdown-capable but DISABLED device (never
|
||||
// provisioned) also has isUnlocked()==false, but it must persist plaintext
|
||||
// normally — skipping here would silently drop every config write (e.g. the
|
||||
// normally - skipping here would silently drop every config write (e.g. the
|
||||
// LoRa region) until the device is provisioned.
|
||||
if (EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked()) {
|
||||
LOG_WARN("NodeDB: saveToDisk skipped — encrypted storage locked");
|
||||
LOG_WARN("NodeDB: saveToDisk skipped - encrypted storage locked");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -3009,7 +3009,7 @@ HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
||||
return HopStartStatus::INVALID;
|
||||
|
||||
if (p.hop_start == 0) {
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition —
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition -
|
||||
// the packet was never meant to travel any hops, so no hop_start ambiguity applies.
|
||||
if (p.hop_limit == 0)
|
||||
return HopStartStatus::VALID;
|
||||
@@ -3204,7 +3204,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
|
||||
if (contact.should_ignore) {
|
||||
// Block the contact and drop its rich satellite data, but keep the
|
||||
// public key copied above — an ignored peer keeps a usable identity
|
||||
// public key copied above - an ignored peer keeps a usable identity
|
||||
// (a verifiable target) rather than a bare node number.
|
||||
if (!setProtectedFlag(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true))
|
||||
LOG_WARN(PROTECTED_CAP_WARN_FMT, "ignore", contact.node_num, MAX_NUM_NODES - 2);
|
||||
@@ -3355,7 +3355,7 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Only sample genuine RF-origin packets. The transport check excludes packets received
|
||||
// directly from the broker (TRANSPORT_MQTT), but an MQTT-origin packet rebroadcast onto
|
||||
// LoRa by a gateway arrives as TRANSPORT_LORA with via_mqtt set — count those would
|
||||
// LoRa by a gateway arrives as TRANSPORT_LORA with via_mqtt set - count those would
|
||||
// inflate the local mesh-size estimate with non-RF nodes (and they usually carry
|
||||
// hop_start==0, landing in the hop-0 bucket that pulls the recommendation lowest), so
|
||||
// exclude via_mqtt too.
|
||||
@@ -3706,7 +3706,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Don't append past the end of the vector. The protected-node cap
|
||||
// (numProtectedNodes() <= MAX_NUM_NODES-2) means the eviction above frees
|
||||
// a slot in normal operation; this guards the legacy case of a pre-cap
|
||||
// database that is full of protected nodes — refuse rather than overrun.
|
||||
// database that is full of protected nodes - refuse rather than overrun.
|
||||
if (numMeshNodes >= MAX_NUM_NODES)
|
||||
return NULL;
|
||||
// Pre-size before append when run before nodeDBSelfCare() (boot keygen); else at() aborts on nRF52.
|
||||
|
||||
+6
-6
@@ -337,11 +337,11 @@ class NodeDB
|
||||
WarmNodeStore warmStore;
|
||||
#endif
|
||||
|
||||
/// Copy the 32-byte public key for node n — hot store first, then the warm
|
||||
/// Copy the 32-byte public key for node n - hot store first, then the warm
|
||||
/// tier. Returns false if we don't know a key for n.
|
||||
bool copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
/// Resolve a node's device role — hot store (with user) first, then the role
|
||||
/// Resolve a node's device role - hot store (with user) first, then the role
|
||||
/// cached in the warm tier, else CLIENT. Lets role-aware policy keep firing for
|
||||
/// nodes that have aged out of the hot store.
|
||||
meshtastic_Config_DeviceConfig_Role getNodeRole(NodeNum n);
|
||||
@@ -412,7 +412,7 @@ class NodeDB
|
||||
emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
|
||||
size_t nodeDatabaseSize;
|
||||
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
|
||||
// Decode-stream size ceiling only — no buffer this big is allocated (load
|
||||
// Decode-stream size ceiling only - no buffer this big is allocated (load
|
||||
// streams from the file). Sized for the largest file any prior firmware
|
||||
// could write (250-node ESP32-S3, satellites uncapped) so capacity
|
||||
// downgrades / peer backups still decode; excess is trimmed after load.
|
||||
@@ -480,7 +480,7 @@ class NodeDB
|
||||
/// Returns true iff every encrypted file decrypted and decoded cleanly.
|
||||
/// On false the caller MUST treat the storage as corrupt: leave the
|
||||
/// connection unauthenticated, emit a LOCKED(storage_corrupt) status,
|
||||
/// and refuse to call setAdminAuthorized — otherwise a subsequent
|
||||
/// and refuse to call setAdminAuthorized - otherwise a subsequent
|
||||
/// set_config would re-encrypt a wrong baseline (the locked-default
|
||||
/// values still resident in `config` / `channelFile` / `nodeDatabase`)
|
||||
/// and overwrite the operator's persisted state.
|
||||
@@ -489,7 +489,7 @@ class NodeDB
|
||||
/// Disable lockdown: decrypt every encrypted pref file back to plaintext,
|
||||
/// then remove the DEK / token / counter / backoff artifacts. Requires
|
||||
/// EncryptedStorage to be unlocked (DEK in RAM). Returns false if any
|
||||
/// file failed to revert — in which case the DEK is still present and the
|
||||
/// file failed to revert - in which case the DEK is still present and the
|
||||
/// device remains in lockdown so the operator can retry. APPROTECT is not
|
||||
/// reversed. Called from the main loop via lockdownDisablePending.
|
||||
bool disableLockdownToPlaintext();
|
||||
@@ -571,7 +571,7 @@ class NodeDB
|
||||
bool migrateLegacyNodeDatabase();
|
||||
|
||||
// Route satellite-store decode entries straight into our maps instead of
|
||||
// temp vectors. Must be paired — disarm before any other NodeDatabase decode.
|
||||
// temp vectors. Must be paired - disarm before any other NodeDatabase decode.
|
||||
void armNodeDatabaseDecodeTargets();
|
||||
void disarmNodeDatabaseDecodeTargets();
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ bool NodeDB::migrateLegacyNodeDatabase()
|
||||
sanitizeUtf8(slim.long_name, sizeof(slim.long_name)); // replace bad bytes so nanopb encode never fails
|
||||
strncpy(slim.short_name, legacy.user.short_name, sizeof(slim.short_name));
|
||||
slim.short_name[sizeof(slim.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(slim.short_name, sizeof(slim.short_name)); // same — v24 names may contain non-UTF-8 bytes
|
||||
sanitizeUtf8(slim.short_name, sizeof(slim.short_name)); // same - v24 names may contain non-UTF-8 bytes
|
||||
slim.hw_model = legacy.user.hw_model;
|
||||
slim.role = legacy.user.role;
|
||||
if (legacy.user.is_licensed)
|
||||
|
||||
@@ -248,7 +248,7 @@ void PacketHistory::hashRemove(NodeNum sender, PacketId id)
|
||||
return;
|
||||
uint16_t idx = hashIndex[bucket];
|
||||
if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) {
|
||||
// Found it — delete and re-insert subsequent entries to maintain probe chain integrity
|
||||
// Found it - delete and re-insert subsequent entries to maintain probe chain integrity
|
||||
hashIndex[bucket] = HASH_EMPTY;
|
||||
uint32_t next = (bucket + 1) & hashMask;
|
||||
for (uint32_t j = 0; j < hashCapacity; j++) {
|
||||
@@ -488,7 +488,7 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N
|
||||
/* Check if a certain node was a relayer of a packet in the history given iterator
|
||||
* @return true if node was indeed a relayer, false if not
|
||||
* NOTE: intentionally byte-domain. Both `relayer` and relayed_by[] are on-wire last bytes, so this
|
||||
* answers "did a relayer with this byte touch the packet" — correct without resolving to a NodeNum.
|
||||
* answers "did a relayer with this byte touch the packet" - correct without resolving to a NodeNum.
|
||||
* The collision risk is neutralized where the result is consumed (route learning in
|
||||
* NextHopRouter::sniffReceived now gates the write through NodeDB::resolveUniqueLastByte). */
|
||||
bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole)
|
||||
|
||||
+52
-52
@@ -64,12 +64,12 @@ static constexpr size_t MAX_AUTH_SLOTS = 6;
|
||||
// status produced for connection A (e.g. UNLOCKED with the active TTL,
|
||||
// or UNLOCK_FAILED with a backoff) cannot be drained by connection B,
|
||||
// which would otherwise learn that A just authenticated or just failed
|
||||
// — a real information leak across local clients.
|
||||
// - a real information leak across local clients.
|
||||
//
|
||||
// File-scope rather than a per-PhoneAPI member because adding any
|
||||
// non-trivial state directly to PhoneAPI broke USB-CDC enumeration on
|
||||
// the current nRF52 framework; the auth-slot table next door uses the
|
||||
// same workaround. Lifecycle is tied to the auth slot table — both are
|
||||
// same workaround. Lifecycle is tied to the auth slot table - both are
|
||||
// keyed by PhoneAPI*, both are cleared together in clearAuthSlot_LH,
|
||||
// and both share g_authSlotsMutex.
|
||||
struct PendingStatusSlot {
|
||||
@@ -78,7 +78,7 @@ struct PendingStatusSlot {
|
||||
bool hasPending = false;
|
||||
// True between a successful passphrase verify and the main-loop
|
||||
// reloadFromDisk that follows. While set, the connection is NOT
|
||||
// yet authorized and no UNLOCKED status has been emitted — the
|
||||
// yet authorized and no UNLOCKED status has been emitted - the
|
||||
// client still sees LOCKED, and any admin op it tries is dropped
|
||||
// by the existing unauth gates. Cleared either way by
|
||||
// completePendingUnlocks once reload finishes.
|
||||
@@ -107,7 +107,7 @@ static PendingStatusSlot *findOrAllocStatusSlot_LH(PhoneAPI *p)
|
||||
// Mirror the auth-slot eviction policy: stale slots can be reused.
|
||||
// A connection that lost its auth slot has nothing meaningful to be
|
||||
// told via a pending status anyway. Never evict a slot mid-unlock
|
||||
// (pendingUnlockAfterReload set) — completing that flow on the
|
||||
// (pendingUnlockAfterReload set) - completing that flow on the
|
||||
// wrong PhoneAPI would authorize the wrong connection.
|
||||
for (auto &s : g_statusSlots) {
|
||||
if (!s.hasPending && !s.pendingUnlockAfterReload) {
|
||||
@@ -143,7 +143,7 @@ static void buildStatus_LH(meshtastic_LockdownStatus &out, meshtastic_LockdownSt
|
||||
memset(&out, 0, sizeof(out));
|
||||
out.state = state;
|
||||
// Collapse the specific token_* reasons to a generic "locked" over
|
||||
// the wire — full detail still goes to local logs. An unauth client
|
||||
// the wire - full detail still goes to local logs. An unauth client
|
||||
// does not need to know whether HMAC failed vs the boot count
|
||||
// hit zero vs the file was the wrong size; all of those mean the
|
||||
// same thing to the client ("locked, ask for passphrase") but
|
||||
@@ -171,7 +171,7 @@ struct PhoneAuthSlot {
|
||||
static PhoneAuthSlot g_authSlots[MAX_AUTH_SLOTS];
|
||||
|
||||
// Global auth epoch. Lock Now bumps it; per-slot `epoch` compared against
|
||||
// this. Wraps at 2^32 revocations — practically unreachable; on wrap the
|
||||
// this. Wraps at 2^32 revocations - practically unreachable; on wrap the
|
||||
// only behavioral effect is that any slot whose epoch happens to match the
|
||||
// new low value would be treated as authorized again, which requires a
|
||||
// pre-existing authorized slot to survive 2^32 lockNow events on the same
|
||||
@@ -179,7 +179,7 @@ static PhoneAuthSlot g_authSlots[MAX_AUTH_SLOTS];
|
||||
static uint32_t g_authEpoch = 1;
|
||||
|
||||
// Single mutex guarding g_authSlots and g_authEpoch. All readers and
|
||||
// writers — including const getters like getAdminAuthorized — must take
|
||||
// writers - including const getters like getAdminAuthorized - must take
|
||||
// it. Granularity is fine because the critical sections are short (a
|
||||
// fixed-size linear scan over 6 entries) and contention is dominated by
|
||||
// getFromRadio's per-call redaction checks, which tolerate brief
|
||||
@@ -191,7 +191,7 @@ static concurrency::Lock g_authSlotsMutex;
|
||||
// evicts the first unauthorized slot found. Refuses to evict an authorized
|
||||
// slot (those represent a live operator session and must outlive the table
|
||||
// pressure of reconnect churn). Returns nullptr only if every slot is
|
||||
// occupied by a different live, authorized PhoneAPI — practically only
|
||||
// occupied by a different live, authorized PhoneAPI - practically only
|
||||
// reachable as a DoS via 7+ simultaneous authed connections, in which
|
||||
// case fail-closed and log.
|
||||
static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
|
||||
@@ -211,7 +211,7 @@ static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
|
||||
}
|
||||
}
|
||||
// Second pass: evict an unauthorized stale slot. Don't touch authorized
|
||||
// ones — those still represent an operator-authenticated session.
|
||||
// ones - those still represent an operator-authenticated session.
|
||||
for (auto &s : g_authSlots) {
|
||||
if (!s.authorized) {
|
||||
s.who = p;
|
||||
@@ -312,7 +312,7 @@ void PhoneAPI::handleStartConfig()
|
||||
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
|
||||
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
|
||||
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
|
||||
// that don't support OTA browsing — the manifest is only consumed by
|
||||
// that don't support OTA browsing - the manifest is only consumed by
|
||||
// companion apps for those flows.
|
||||
releaseFilesManifest(filesManifest);
|
||||
#else
|
||||
@@ -444,12 +444,12 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
case meshtastic_ToRadio_packet_tag:
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
// Allow admin messages addressed to this device — passphrase delivery must get through.
|
||||
// Allow admin messages addressed to this device - passphrase delivery must get through.
|
||||
// AdminModule handles its own is_managed gate for those.
|
||||
// Block everything else — unauthorized clients cannot inject mesh traffic.
|
||||
// Block everything else - unauthorized clients cannot inject mesh traffic.
|
||||
// Require the packet to carry a decoded (not encrypted) payload so portnum is valid.
|
||||
// Refuse to match when our own node number is still 0 (NodeDB
|
||||
// not yet loaded — happens during the locked-default boot path
|
||||
// not yet loaded - happens during the locked-default boot path
|
||||
// before reloadFromDisk). Otherwise a packet with to==0 would
|
||||
// satisfy the equality and bypass the gate.
|
||||
NodeNum ourNum = nodeDB->getNodeNum();
|
||||
@@ -584,12 +584,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.my_info = myNodeInfo;
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
// device_id is a stable hardware identifier — useful for an attacker
|
||||
// device_id is a stable hardware identifier - useful for an attacker
|
||||
// to fingerprint / correlate the device across observations. Strip it
|
||||
// for unauthenticated clients. my_node_num is kept (it's broadcast
|
||||
// on the mesh anyway). pio_env / min_app_version reveal the exact
|
||||
// build flavour, useful only for picking which known-CVE to try.
|
||||
// nodedb_count stays — clients need it to decide whether to pull
|
||||
// nodedb_count stays - clients need it to decide whether to pull
|
||||
// the node DB after unlocking.
|
||||
fromRadioScratch.my_info.device_id.size = 0;
|
||||
memset(fromRadioScratch.my_info.device_id.bytes, 0, sizeof(fromRadioScratch.my_info.device_id.bytes));
|
||||
@@ -657,7 +657,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// position_flags, excluded_modules, optionsCount. None of it
|
||||
// is needed to drive lockdown_auth, and most of it tells an
|
||||
// attacker which CVE / behavior quirks to probe. Wipe the
|
||||
// whole struct — clients re-fetch once authenticated.
|
||||
// whole struct - clients re-fetch once authenticated.
|
||||
memset(&fromRadioScratch.metadata, 0, sizeof(fromRadioScratch.metadata));
|
||||
}
|
||||
#endif
|
||||
@@ -668,7 +668,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// Tell the client which modem presets are legal in each region so its UI
|
||||
// can block illegal region+preset combinations. This is public RF /
|
||||
// regulatory information (region and modem_preset are already in the
|
||||
// unauthenticated LoRa whitelist below), so it is sent unconditionally —
|
||||
// unauthenticated LoRa whitelist below), so it is sent unconditionally -
|
||||
// even an unauthorized/locked-down client can render a correct picker.
|
||||
LOG_DEBUG("Send region preset map");
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_region_presets_tag;
|
||||
@@ -683,7 +683,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
if (!getAdminAuthorized()) {
|
||||
// Unauthenticated: emit a zero-initialized Channel. fromRadioScratch
|
||||
// was memset(0) at the top of getFromRadio(), so leaving .channel
|
||||
// untouched gives the client an empty entry — no name, no PSK, no
|
||||
// untouched gives the client an empty entry - no name, no PSK, no
|
||||
// role. Advances the state machine normally so config_complete_id
|
||||
// still fires.
|
||||
} else
|
||||
@@ -748,7 +748,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// private knobs (ignore_incoming list, override_duty_cycle,
|
||||
// override_frequency, sx126x_rx_boosted_gain, tx_power,
|
||||
// ignore_mqtt, fem_lna_mode, config_ok_to_mqtt, ...) stay
|
||||
// hidden — they tell an attacker how the operator has tuned
|
||||
// hidden - they tell an attacker how the operator has tuned
|
||||
// the device but are not needed by an unauth client.
|
||||
meshtastic_Config_LoRaConfig whitelist = {};
|
||||
whitelist.use_preset = config.lora.use_preset;
|
||||
@@ -775,7 +775,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
if (!getAdminAuthorized()) {
|
||||
// Unauthenticated: emit an empty SecurityConfig (zero-init from
|
||||
// the top-of-loop memset). No private_key, no admin_keys, no
|
||||
// public_key — nothing for an attacker to inspect.
|
||||
// public_key - nothing for an attacker to inspect.
|
||||
//
|
||||
// Provisioning state (NEEDS_PROVISION vs LOCKED) is conveyed via
|
||||
// the FromRadio.lockdown_status proto sent post-config; clients
|
||||
@@ -906,7 +906,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
// Unauthorized client: skip node DB and file manifest — only send config complete
|
||||
// Unauthorized client: skip node DB and file manifest - only send config complete
|
||||
state = STATE_SEND_COMPLETE_ID;
|
||||
} else
|
||||
#endif
|
||||
@@ -993,7 +993,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else if (mqttClientProxyMessageForPhone) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
releaseMqttClientProxyPhonePacket(); // Discard — unauthorized client
|
||||
releaseMqttClientProxyPhonePacket(); // Discard - unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1004,7 +1004,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard — unauthorized client
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard - unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1015,7 +1015,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
} else if (hasPendingLockdownStatus()) {
|
||||
concurrency::LockGuard guard(&g_authSlotsMutex);
|
||||
// Look up our own slot only — never another connection's. Re-check
|
||||
// Look up our own slot only - never another connection's. Re-check
|
||||
// hasPending under the lock since a concurrent drain on the same
|
||||
// connection (unlikely but possible if multiple transport
|
||||
// callbacks race against one PhoneAPI) may have grabbed it.
|
||||
@@ -1033,7 +1033,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else if (packetForPhone) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
releasePhonePacket(); // Discard mesh traffic — unauthorized client
|
||||
releasePhonePacket(); // Discard mesh traffic - unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1044,7 +1044,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
releasePhonePacket();
|
||||
}
|
||||
} else if (replayPending()) {
|
||||
// No live packet pending — feed the phone one cached satellite-DB packet.
|
||||
// No live packet pending - feed the phone one cached satellite-DB packet.
|
||||
// popReplayPacket advances through positions->telemetry->environment->status,
|
||||
// and flips replayPhase back to IDLE when everything has been drained.
|
||||
meshtastic_MeshPacket replayPkt;
|
||||
@@ -1222,7 +1222,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated — iOS client filters
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
@@ -1327,7 +1327,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated — iOS client filters
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
@@ -1391,7 +1391,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated — client filters
|
||||
// Mark as if heard over the air, not internally generated - client filters
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
|
||||
@@ -1483,7 +1483,7 @@ bool PhoneAPI::popReplayPacket(meshtastic_MeshPacket &out)
|
||||
}
|
||||
}
|
||||
|
||||
// Queue empty AND no more entries to feed it — phase is exhausted.
|
||||
// Queue empty AND no more entries to feed it - phase is exhausted.
|
||||
advanceReplayPhase();
|
||||
}
|
||||
return false;
|
||||
@@ -1609,7 +1609,7 @@ bool PhoneAPI::available()
|
||||
hasPacket = !!packetForPhone;
|
||||
if (hasPacket)
|
||||
return true;
|
||||
// Trailing replay drain — feeds cached satellite-DB packets alongside
|
||||
// Trailing replay drain - feeds cached satellite-DB packets alongside
|
||||
// (lower priority than) live traffic.
|
||||
return replayPending();
|
||||
}
|
||||
@@ -1666,7 +1666,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
// (b) Any other admin payload from an unauthorized connection:
|
||||
// dropped here. The previous design relied on AdminModule
|
||||
// to apply isLocalAdminAuthorized() during dispatch, but
|
||||
// AdminModule runs on the Router task — by then the
|
||||
// AdminModule runs on the Router task - by then the
|
||||
// PhoneAPI dispatching task has already exited and the
|
||||
// per-connection auth context is unrecoverable. Putting
|
||||
// the gate here closes that race and covers H6/H7 from the
|
||||
@@ -1678,7 +1678,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
|
||||
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// Wipe the decoded passphrase scratch — the byte array in
|
||||
// Wipe the decoded passphrase scratch - the byte array in
|
||||
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
@@ -1769,7 +1769,7 @@ bool PhoneAPI::getAdminAuthorized() const
|
||||
{
|
||||
// Runtime-toggle model: when lockdown is NOT active (a lockdown-capable
|
||||
// build that hasn't been provisioned, or that was disabled), there is
|
||||
// nothing to protect — every connection is implicitly authorized, so
|
||||
// nothing to protect - every connection is implicitly authorized, so
|
||||
// all the `if (!getAdminAuthorized())` redaction gates throughout
|
||||
// getFromRadio() / handleToRadio() become no-ops and the device serves
|
||||
// config exactly like stock firmware. Only once provisioned (lockdown
|
||||
@@ -1779,7 +1779,7 @@ bool PhoneAPI::getAdminAuthorized() const
|
||||
return true;
|
||||
#endif
|
||||
concurrency::LockGuard g(&g_authSlotsMutex);
|
||||
// const_cast is safe — findOrAllocSlot_LH only mutates the slot table,
|
||||
// const_cast is safe - findOrAllocSlot_LH only mutates the slot table,
|
||||
// not the PhoneAPI itself, and the table key is just the pointer.
|
||||
const auto *slot = findOrAllocSlot_LH(const_cast<PhoneAPI *>(this));
|
||||
return slot && slot->authorized && slot->epoch == g_authEpoch;
|
||||
@@ -1790,7 +1790,7 @@ void PhoneAPI::setAdminAuthorized(bool authorized)
|
||||
concurrency::LockGuard g(&g_authSlotsMutex);
|
||||
auto *slot = findOrAllocSlot_LH(this);
|
||||
if (!slot)
|
||||
return; // slot table full — fail-closed
|
||||
return; // slot table full - fail-closed
|
||||
if (authorized) {
|
||||
slot->epoch = g_authEpoch;
|
||||
slot->authorized = true;
|
||||
@@ -1813,7 +1813,7 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
|
||||
{
|
||||
// Snapshot fields that we'll need outside the lock (we cannot call
|
||||
// EncryptedStorage / setAdminAuthorized / unlockScreen while holding
|
||||
// g_authSlotsMutex without risking re-entry — setAdminAuthorized
|
||||
// g_authSlotsMutex without risking re-entry - setAdminAuthorized
|
||||
// itself takes the same lock).
|
||||
constexpr size_t kMaxSnapshots = MAX_AUTH_SLOTS;
|
||||
PhoneAPI *targets[kMaxSnapshots] = {};
|
||||
@@ -1825,7 +1825,7 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
|
||||
continue;
|
||||
if (targetCount < kMaxSnapshots)
|
||||
targets[targetCount++] = s.who;
|
||||
// Clear the pending flag either way — failure path must not
|
||||
// Clear the pending flag either way - failure path must not
|
||||
// leave it set so a subsequent successful reload retries
|
||||
// against the wrong PhoneAPI.
|
||||
s.pendingUnlockAfterReload = false;
|
||||
@@ -1841,13 +1841,13 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
|
||||
p->queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCKED, "", boots, until, 0);
|
||||
}
|
||||
// Screen-lock latch is cleared once any client successfully
|
||||
// unlocks — the operator has proven the passphrase. Matches the
|
||||
// unlocks - the operator has proven the passphrase. Matches the
|
||||
// re-verify path's behavior.
|
||||
if (targetCount > 0)
|
||||
meshtastic_security::unlockScreen();
|
||||
LOG_INFO("Lockdown: post-reload completion: authorized %u connection(s)", (unsigned)targetCount);
|
||||
} else {
|
||||
// Storage corrupt — emit LOCKED(storage_corrupt) to every slot
|
||||
// Storage corrupt - emit LOCKED(storage_corrupt) to every slot
|
||||
// that was awaiting the unlock. setAdminAuthorized is NOT called
|
||||
// so the connection stays redacted and any set_config it sends
|
||||
// is dropped at the existing unauth gates. Caller (main.cpp) has
|
||||
@@ -1866,7 +1866,7 @@ void PhoneAPI::queueLockdownStatus(meshtastic_LockdownStatus_State state, const
|
||||
concurrency::LockGuard guard(&g_authSlotsMutex);
|
||||
auto *slot = findOrAllocStatusSlot_LH(this);
|
||||
if (!slot)
|
||||
return; // slot table exhausted — fail-closed, no status delivered
|
||||
return; // slot table exhausted - fail-closed, no status delivered
|
||||
buildStatus_LH(slot->status, state, lock_reason, boots_remaining, valid_until_epoch, backoff_seconds);
|
||||
slot->hasPending = true;
|
||||
}
|
||||
@@ -1914,14 +1914,14 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
ppVol[zi] = 0;
|
||||
};
|
||||
|
||||
// Lock Now — only honored from a connection that has already proven
|
||||
// Lock Now - only honored from a connection that has already proven
|
||||
// the passphrase. Unauthenticated clients used to be able to trigger
|
||||
// a reboot, which was a trivial local-presence DoS (any BLE/USB
|
||||
// attacker could brick-loop the device). Now lock_now requires
|
||||
// prior auth on this connection.
|
||||
if (la.lock_now) {
|
||||
if (!getAdminAuthorized()) {
|
||||
LOG_WARN("Lockdown: LOCK NOW from unauthorized connection — denied");
|
||||
LOG_WARN("Lockdown: LOCK NOW from unauthorized connection - denied");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
@@ -1938,18 +1938,18 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
// Disable lockdown entirely. Requires the passphrase (must prove
|
||||
// ownership before reverting at-rest encryption). We verify it here to
|
||||
// load the DEK, then hand the heavy decrypt-revert work to the main
|
||||
// loop via lockdownDisablePending — exactly like the unlock reload
|
||||
// loop via lockdownDisablePending - exactly like the unlock reload
|
||||
// path, because decrypting + rewriting nodes.proto is too heavy for
|
||||
// this transport-callback stack. APPROTECT is NOT reversed.
|
||||
if (la.disable) {
|
||||
if (la.passphrase.size < 1) {
|
||||
LOG_WARN("Lockdown: disable with empty passphrase — rejecting");
|
||||
LOG_WARN("Lockdown: disable with empty passphrase - rejecting");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
}
|
||||
if (!EncryptedStorage::isLockdownActive()) {
|
||||
// Already off — nothing to do; report DISABLED so the client UI settles.
|
||||
// Already off - nothing to do; report DISABLED so the client UI settles.
|
||||
LOG_INFO("Lockdown: disable requested but lockdown is not active");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
@@ -1972,13 +1972,13 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty-passphrase auth was previously a silent success — clients
|
||||
// Empty-passphrase auth was previously a silent success - clients
|
||||
// got no feedback and the device looked the same as it would after
|
||||
// an actual no-op. Emit UNLOCK_FAILED with no backoff so honest
|
||||
// clients can detect their own bug and an attacker still learns
|
||||
// nothing they wouldn't from any other failed attempt.
|
||||
if (la.passphrase.size < 1) {
|
||||
LOG_WARN("Lockdown: lockdown_auth with empty passphrase and lock_now=false — rejecting");
|
||||
LOG_WARN("Lockdown: lockdown_auth with empty passphrase and lock_now=false - rejecting");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
@@ -2017,7 +2017,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
if (ok) {
|
||||
needsReload = true;
|
||||
// Mark this slot for the main-loop completion handler. Don't
|
||||
// authorize or emit UNLOCKED yet — `config` / `channelFile`
|
||||
// authorize or emit UNLOCKED yet - `config` / `channelFile`
|
||||
// / `nodeDatabase` still hold the locked-default placeholders
|
||||
// installed by loadFromDisk()'s !isUnlocked() branch. If we
|
||||
// flipped the connection to authorized here, the client could
|
||||
@@ -2040,7 +2040,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
ok = EncryptedStorage::unlockWithPassphrase(la.passphrase.bytes, la.passphrase.size, boots, validUntilEpoch,
|
||||
sessionMaxSeconds);
|
||||
if (ok) {
|
||||
// Storage was already unlocked — no reload needed. Authorize
|
||||
// Storage was already unlocked - no reload needed. Authorize
|
||||
// and surface UNLOCKED to the client immediately.
|
||||
setAdminAuthorized(true);
|
||||
LOG_INFO("Lockdown: passphrase verified, this connection authorized");
|
||||
@@ -2055,13 +2055,13 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCKED, "", EncryptedStorage::getBootsRemaining(),
|
||||
EncryptedStorage::getValidUntilEpoch(), 0);
|
||||
} else if (ok && needsReload) {
|
||||
// Cold-unlock path: deliberately no status emission yet — the
|
||||
// Cold-unlock path: deliberately no status emission yet - the
|
||||
// client keeps seeing LOCKED until completePendingUnlocks()
|
||||
// runs after a successful reload.
|
||||
} else {
|
||||
uint32_t backoff = EncryptedStorage::getBackoffSecondsRemaining();
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, backoff);
|
||||
// Don't log backoff seconds — the client receives it in the
|
||||
// Don't log backoff seconds - the client receives it in the
|
||||
// UNLOCK_FAILED status anyway, and in non-DEBUG_MUTE builds the
|
||||
// numeric value would otherwise spill onto a USB-attached
|
||||
// attacker's serial terminal alongside other diagnostic noise.
|
||||
|
||||
+4
-4
@@ -175,7 +175,7 @@ class PhoneAPI
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
/// Per-connection auth: tracked in a small file-scope slot table keyed
|
||||
/// by PhoneAPI*. Adding state members directly to PhoneAPI broke
|
||||
/// USB-CDC enumeration on current nRF52 framework — even one extra
|
||||
/// USB-CDC enumeration on current nRF52 framework - even one extra
|
||||
/// per-instance uint32_t was enough. Keeping all state out-of-line
|
||||
/// avoids the issue.
|
||||
void setAdminAuthorized(bool authorized);
|
||||
@@ -257,10 +257,10 @@ class PhoneAPI
|
||||
APIType api_type = TYPE_NONE;
|
||||
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// No per-instance auth members — see method-level note. All state lives
|
||||
// No per-instance auth members - see method-level note. All state lives
|
||||
// in a file-scope slot table in PhoneAPI.cpp keyed by `this` pointer.
|
||||
|
||||
// Pending LockdownStatus storage is NOT a class member — having a
|
||||
// Pending LockdownStatus storage is NOT a class member - having a
|
||||
// meshtastic_LockdownStatus (~50 bytes with the char[33] lock_reason)
|
||||
// as a PhoneAPI member broke USB-CDC enumeration on the nRF52 Adafruit
|
||||
// framework. The exact mechanism wasn't pinned down, but moving the
|
||||
@@ -310,7 +310,7 @@ class PhoneAPI
|
||||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||||
/// Synchronously handle a lockdown_auth AdminMessage from the local
|
||||
/// client. Runs inside handleToRadioPacket so the originating
|
||||
/// connection is reachable via `this` — avoids the async context
|
||||
/// connection is reachable via `this` - avoids the async context
|
||||
/// loss that broke the previous AdminModule path. Always consumes the
|
||||
/// packet (returns true): lockdown_auth is local-only and must not be
|
||||
/// forwarded to the mesh router.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
// Max precision on a publicly-decryptable channel. CCPA "precise geolocation" = within a ~564m (1,850ft) radius.
|
||||
// Precision is bit-truncation of latitude_i/longitude_i: the latitude cell stays ~constant in meters worldwide
|
||||
// (~700m at 15 bits), while only the longitude cell varies — widest at the equator, narrowing toward the poles.
|
||||
// (~700m at 15 bits), while only the longitude cell varies - widest at the equator, narrowing toward the poles.
|
||||
// 15 also matches the MQTT map-report public precision ceiling.
|
||||
#define MAX_POSITION_PRECISION_PUBLIC_KEY 15
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ const RegionInfo regions[] = {
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
|
||||
Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||||
Thailand 920-925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||||
*/
|
||||
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
@@ -246,7 +246,7 @@ const RegionInfo regions[] = {
|
||||
|
||||
/*
|
||||
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
|
||||
Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this
|
||||
Power limit is the regulatory ceiling (1 W / 30 dBm) - individual hardware will cap below this
|
||||
via its own PA curve; the field here is just the legal upper bound.
|
||||
|
||||
Default slot: 26 (144.510 MHz)
|
||||
|
||||
@@ -371,10 +371,10 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
handleTransmitInterrupt(); // completeSending() already restored the radio to the home config
|
||||
#if !MESHTASTIC_EXCLUDE_BEACON
|
||||
// Pre-switch the radio to the NEXT queued packet's beacon config (no-op for normal traffic).
|
||||
// Not required for correctness — TRANSMIT_DELAY_COMPLETED would switch before CAD anyway — but
|
||||
// Not required for correctness - TRANSMIT_DELAY_COMPLETED would switch before CAD anyway - but
|
||||
// doing it here lets the next beacon skip the switch-only delay cycle and, more importantly,
|
||||
// keeps the post-TX listen window (and the CAD/LBT that follows) on the channel we're about to
|
||||
// transmit on. Only engages when the next packet is itself a beacon — exactly when we want it.
|
||||
// transmit on. Only engages when the next packet is itself a beacon - exactly when we want it.
|
||||
MeshBeaconModule::reconfigureForBeaconTX(this, txQueue.getFront());
|
||||
#endif
|
||||
startReceive();
|
||||
@@ -402,8 +402,8 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
#if !MESHTASTIC_EXCLUDE_BEACON
|
||||
} else if (MeshBeaconModule::beaconTxConfigInvalid(txp)) {
|
||||
// The beacon's target radio config is invalid (bad preset/region, or an
|
||||
// unlicensed node keying up on a ham-only region). Drop the packet — never
|
||||
// transmit it on the current (home) config — and move on to the next queued packet.
|
||||
// unlicensed node keying up on a ham-only region). Drop the packet - never
|
||||
// transmit it on the current (home) config - and move on to the next queued packet.
|
||||
LOG_DEBUG("Beacon: invalid TX radio config, dropping packet 0x%08x", txp->id);
|
||||
meshtastic_MeshPacket *bad = txQueue.dequeue();
|
||||
MeshBeaconModule::clearTargetRadioSettings(bad);
|
||||
|
||||
@@ -160,7 +160,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/**
|
||||
* Reset AGC by power-cycling the analog frontend.
|
||||
* Subclasses override with chip-specific calibration sequences.
|
||||
* Safe to call periodically — skips if currently sending or receiving.
|
||||
* Safe to call periodically - skips if currently sending or receiving.
|
||||
*/
|
||||
virtual void resetAGC();
|
||||
|
||||
|
||||
+2
-2
@@ -100,7 +100,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||
return true;
|
||||
}
|
||||
|
||||
// router_preserve_hops: not suitable right now — removed from config until
|
||||
// router_preserve_hops: not suitable right now - removed from config until
|
||||
// the right heuristics for when to preserve vs. exhaust hops are established.
|
||||
// #if HAS_TRAFFIC_MANAGEMENT
|
||||
// if (moduleConfig.has_traffic_management &&
|
||||
@@ -560,7 +560,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);
|
||||
}
|
||||
} else {
|
||||
// Unsigned packet — only reject the class of packet a signing node always signs:
|
||||
// Unsigned packet - only reject the class of packet a signing node always signs:
|
||||
// an unencrypted broadcast small enough to also carry a signature (see perhapsEncode()).
|
||||
// Unicast packets and oversized broadcasts are never signed, so they must not be
|
||||
// hard-failed here even if this node has signed before.
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
||||
void addInterface(std::unique_ptr<RadioInterface> _iface) { iface = std::move(_iface); }
|
||||
|
||||
/**
|
||||
* Borrowed (non-owning) access to the radio interface — used by NodeDB
|
||||
* Borrowed (non-owning) access to the radio interface - used by NodeDB
|
||||
* after a lockdown unlock so it can push the freshly-loaded config to
|
||||
* the SX12xx via reconfigure(). Returns nullptr when no radio has been
|
||||
* attached (e.g. ARCH_PORTDUINO simulator before SimRadio bind).
|
||||
|
||||
@@ -215,7 +215,7 @@ void StreamAPI::emitRebooted()
|
||||
|
||||
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
|
||||
{
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here — those
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here - those
|
||||
// belong to the main packet-emission path and a LOG_ firing during
|
||||
// `writeStream()` would corrupt an in-flight encode. We keep a
|
||||
// dedicated `fromRadioScratchLog` + `txBufLog` for log records and
|
||||
|
||||
@@ -107,7 +107,7 @@ class StreamAPI : public PhoneAPI
|
||||
/// re-used `fromRadioScratch` / `txBuf` and corrupted whatever the main
|
||||
/// path had already encoded. Symptoms on the host were
|
||||
/// `google.protobuf.message.DecodeError: Error parsing message with type
|
||||
/// 'meshtastic.protobuf.FromRadio'` — any tool with
|
||||
/// 'meshtastic.protobuf.FromRadio'` - any tool with
|
||||
/// `config.security.debug_log_api_enabled=true` under traffic would see
|
||||
/// torn frames every few messages.
|
||||
///
|
||||
|
||||
@@ -87,7 +87,7 @@ void TransmitHistory::setLastSentToMesh(uint16_t key)
|
||||
const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE;
|
||||
history[key] = makeStoredTimestamp(now, flags);
|
||||
dirty = true;
|
||||
// Don't flush to disk on every transmit — flash has limited write endurance.
|
||||
// Don't flush to disk on every transmit - flash has limited write endurance.
|
||||
// The in-memory lastMillis map handles throttle during normal operation.
|
||||
// Disk is flushed: before deep sleep (sleep.cpp) and periodically here,
|
||||
// throttled to at most once per 5 minutes. Always save the first time
|
||||
@@ -189,7 +189,7 @@ uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const
|
||||
// Fall back to epoch conversion (loaded from disk after reboot)
|
||||
auto it = history.find(key);
|
||||
if (it == history.end() || it->second.seconds == 0) {
|
||||
return 0; // No stored time — module has never sent
|
||||
return 0; // No stored time - module has never sent
|
||||
}
|
||||
|
||||
// Convert to a millis()-relative timestamp: millis() - msAgo.
|
||||
@@ -271,7 +271,7 @@ void TransmitHistory::clear()
|
||||
}
|
||||
|
||||
#else
|
||||
// No filesystem available — provide stub with in-memory tracking
|
||||
// No filesystem available - provide stub with in-memory tracking
|
||||
TransmitHistory *transmitHistory = nullptr;
|
||||
|
||||
TransmitHistory *TransmitHistory::getInstance()
|
||||
|
||||
@@ -78,7 +78,7 @@ class TransmitHistory
|
||||
|
||||
/**
|
||||
* Wipe in-memory throttle state + remove the on-disk file. Required
|
||||
* alongside rmDir("/prefs") in factoryReset — otherwise the 5-min
|
||||
* alongside rmDir("/prefs") in factoryReset - otherwise the 5-min
|
||||
* auto-flush resurrects the file from the still-populated maps.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
@@ -94,7 +94,7 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
|
||||
position.time = lite.time;
|
||||
// Preserve the peer's broadcast precision; falls back to 0 for entries cached
|
||||
// before the precision_bits field existed in PositionLite (pre-migration data).
|
||||
// iOS treats 0 as "unspecified precision" and won't render the pin — so for
|
||||
// iOS treats 0 as "unspecified precision" and won't render the pin - so for
|
||||
// unset values, declare full precision so the stored lat/lon renders as a point.
|
||||
position.precision_bits = lite.precision_bits == 0 ? 32 : lite.precision_bits;
|
||||
|
||||
|
||||
+11
-11
@@ -12,13 +12,13 @@
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
#include "flash/flash_nrf5x.h"
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" — v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" — v1: last_heard was a plain timestamp.
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" - v1: last_heard was a plain timestamp.
|
||||
// v1 pages are still read on upgrade: we keep each record's identity + public key but
|
||||
// DISCARD its last_heard (the old timestamp would be misread as role/protected bits).
|
||||
// Records re-rank and re-learn their role on the next contact. Legacy pages convert to
|
||||
// v2 naturally as the ring rotates.
|
||||
// A tombstone is an entry record whose last_heard is all-ones — getTime()
|
||||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
||||
#define WARM_RING_TOMBSTONE 0xFFFFFFFFu
|
||||
@@ -33,9 +33,9 @@ struct WarmStoreHeader {
|
||||
};
|
||||
static_assert(sizeof(WarmStoreHeader) == 16, "header layout is part of the persistence format");
|
||||
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" — v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC_V1 \
|
||||
0x314D5257u // "WRM1" — v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
// identity + key but discard last_heard, then rewrite as v2.
|
||||
|
||||
#ifdef FSCom
|
||||
@@ -104,7 +104,7 @@ WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8
|
||||
slot = &e;
|
||||
break;
|
||||
}
|
||||
// Compare on the time bits only — the low metadata bits (role/protected) must
|
||||
// Compare on the time bits only - the low metadata bits (role/protected) must
|
||||
// not perturb LRU victim selection.
|
||||
if (keyIsSet(e.public_key)) {
|
||||
if (!oldestKeyed || warmTimeOf(e) < warmTimeOf(*oldestKeyed))
|
||||
@@ -252,7 +252,7 @@ bool WarmNodeStore::saveIfDirty()
|
||||
// 3 × 4 KB pages below LittleFS. Mutations append 40 B records (entry snapshot,
|
||||
// or tombstone with last_heard == 0xFFFFFFFF) via the shared flash_nrf5x page
|
||||
// cache; saveIfDirty() is the durability point. A full page reclaims the oldest
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock —
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||||
// the page cache is shared with InternalFS/LittleFS.
|
||||
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy) const
|
||||
@@ -289,7 +289,7 @@ void WarmNodeStore::ringOpenPage(uint8_t page)
|
||||
}
|
||||
|
||||
// Caller holds spiLock. May recurse once via ringAppend if the stranded set
|
||||
// fills the fresh page exactly — bounded by WARM_NODE_COUNT <= 2*kRecordsPerPage.
|
||||
// fills the fresh page exactly - bounded by WARM_NODE_COUNT <= 2*kRecordsPerPage.
|
||||
void WarmNodeStore::ringRotate()
|
||||
{
|
||||
uint8_t target = 0;
|
||||
@@ -441,7 +441,7 @@ void WarmNodeStore::load()
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
} else {
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp —
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp -
|
||||
// its low bits would otherwise be misread as role/protected metadata.
|
||||
uint32_t lh = rec.last_heard;
|
||||
if (legacy) {
|
||||
@@ -459,7 +459,7 @@ void WarmNodeStore::load()
|
||||
nextSeq = seqs[k] + 1;
|
||||
// If the head is a v1 page, force the next append to rotate into a fresh v2 page,
|
||||
// so new (v2) records never land in a page whose header says v1 (which would make
|
||||
// a later load discard their last_heard — including the role/protected we just set).
|
||||
// a later load discard their last_heard - including the role/protected we just set).
|
||||
if (legacy)
|
||||
writeSlot = kRecordsPerPage;
|
||||
}
|
||||
@@ -521,7 +521,7 @@ void WarmNodeStore::load()
|
||||
{
|
||||
if (!entries)
|
||||
return;
|
||||
// Clear first — all failure paths below then correctly represent "empty",
|
||||
// Clear first - all failure paths below then correctly represent "empty",
|
||||
// even if load() is called on an already-used instance.
|
||||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||||
concurrency::LockGuard g(spiLock);
|
||||
|
||||
@@ -23,21 +23,21 @@
|
||||
* Warm ("long-tail") node tier.
|
||||
*
|
||||
* Minimal identity record (NodeNum, last_heard, Curve25519 public key) for nodes
|
||||
* evicted from the hot NodeInfoLite store, so DMs to/from them keep encrypting —
|
||||
* evicted from the hot NodeInfoLite store, so DMs to/from them keep encrypting -
|
||||
* the key is expensive to re-learn, the rest rebuilds from traffic in seconds.
|
||||
* Flat fixed array, linear scan (only on hot-store misses), LRU by last_heard
|
||||
* with keyed entries outranking keyless.
|
||||
*
|
||||
* Persistence: nRF52840 uses a 12 KB raw-flash record-ring below LittleFS
|
||||
* (append + replay + compact-on-rotate — see the backend in WarmNodeStore.cpp,
|
||||
* (append + replay + compact-on-rotate - see the backend in WarmNodeStore.cpp,
|
||||
* link-guarded by nrf52840_s140_v7.ld). Everywhere else: /prefs/warm.dat.
|
||||
*/
|
||||
struct WarmNodeEntry {
|
||||
NodeNum num; // 0 = empty slot
|
||||
uint32_t last_heard; // recency for LRU ordering — see the metadata steal below
|
||||
uint32_t last_heard; // recency for LRU ordering - see the metadata steal below
|
||||
uint8_t public_key[32]; // all-zero = no key (a real key is never all-zero)
|
||||
};
|
||||
static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B — persistence format depends on it");
|
||||
static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B - persistence format depends on it");
|
||||
|
||||
// Metadata packed into the low bits of last_heard.
|
||||
//
|
||||
@@ -49,7 +49,7 @@ static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B — per
|
||||
//
|
||||
// Safe because: a real timestamp can never be all-ones (the tombstone sentinel) before
|
||||
// 2106, and tombstones/erased flash are detected via num before last_heard is read. Only
|
||||
// the LOW bits are stolen — the high (era) bits are untouched, so the time range is intact.
|
||||
// the LOW bits are stolen - the high (era) bits are untouched, so the time range is intact.
|
||||
static constexpr uint32_t WARM_META_BITS = 6; // role(4) + protected(2)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x3F → 64 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFFC0
|
||||
|
||||
@@ -26,7 +26,7 @@ static constexpr const char *PROTOBUF_SCHEMA =
|
||||
// PhoneAPI subclass for the Ethernet HTTP transport. Mirrors mesh/http/HttpAPI
|
||||
// but lives outside the MESHTASTIC_EXCLUDE_WEBSERVER gate (which is ESP32-only).
|
||||
// A single instance is shared between the HTTP and HTTPS servers since they
|
||||
// represent the same logical "phone" — same state machine, same packet queue.
|
||||
// represent the same logical "phone" - same state machine, same packet queue.
|
||||
class EthHttpAPI : public PhoneAPI
|
||||
{
|
||||
public:
|
||||
@@ -192,7 +192,7 @@ static bool handleFromRadio(IStreamReadWrite &client, const Request &req)
|
||||
// Buffer all packets first so we can emit an accurate Content-Length and
|
||||
// keep the connection alive. Phase 2 used Connection: close framing, which
|
||||
// forced clients to redo the TLS handshake (~625 ms) for every single
|
||||
// /fromradio poll — client.meshtastic.org needs dozens of those during
|
||||
// /fromradio poll - client.meshtastic.org needs dozens of those during
|
||||
// initial sync, so the user-visible load time was 15-30 s of pure
|
||||
// handshakes. With Content-Length + keep-alive a whole sync rides one
|
||||
// handshake. Buffer is dynamic (std::vector) so the common 1-packet case
|
||||
@@ -290,7 +290,7 @@ void handleApiClient(IStreamReadWrite &client)
|
||||
// the parent OSThread is not returning to mainController, and the
|
||||
// RP2350 hardware watchdog (8 s default in arduino-pico) only gets
|
||||
// pet by the main loop. A client.meshtastic.org sync produces ~80
|
||||
// back-to-back requests over a single TLS session — well past the
|
||||
// back-to-back requests over a single TLS session - well past the
|
||||
// watchdog deadline. yield() between requests lets the rest of core0
|
||||
// (Periodic ticks, NTP, MQTT, LoRa packet pump) run + pets the
|
||||
// watchdog; the cap puts a hard ceiling so a chatty client can never
|
||||
@@ -320,7 +320,7 @@ void handleApiClient(IStreamReadWrite &client)
|
||||
keepAlive = handleToRadio(client, req);
|
||||
} else {
|
||||
sendError(client, 404, "Not Found", "unknown endpoint");
|
||||
return; // errors are terminal — Connection: close framing
|
||||
return; // errors are terminal - Connection: close framing
|
||||
}
|
||||
// A handler that emitted an error advertised Connection: close. Stop the
|
||||
// keep-alive loop so any unread/leftover body bytes (e.g. after a 408
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
// transports without recompiling the handlers.
|
||||
//
|
||||
// Inherits Print so all `print(int)`, `print(const char *)`, `print(char)`
|
||||
// helpers are available for free — the only thing implementations have to
|
||||
// helpers are available for free - the only thing implementations have to
|
||||
// supply on the write side is `write(uint8_t)` + the bulk `write(buf, len)`.
|
||||
class IStreamReadWrite : public Print
|
||||
{
|
||||
public:
|
||||
virtual ~IStreamReadWrite() = default;
|
||||
|
||||
// Write side — Print pure virtual + bulk override
|
||||
// Write side - Print pure virtual + bulk override
|
||||
size_t write(uint8_t b) override = 0;
|
||||
size_t write(const uint8_t *buf, size_t len) override = 0;
|
||||
using Print::write; // bring in write(const char *str) and friends
|
||||
@@ -34,7 +34,7 @@ class IStreamReadWrite : public Print
|
||||
virtual bool connected() = 0;
|
||||
void flush() override = 0; // Print::flush is virtual void with empty default
|
||||
|
||||
// Logging helper — used by request log line
|
||||
// Logging helper - used by request log line
|
||||
virtual IPAddress remoteIP() = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ static EthApiServerThread *apiThread = nullptr;
|
||||
|
||||
void initEthApiServer()
|
||||
{
|
||||
// Bind the listener (idempotent — deInitEthApiServer() drops apiServer on a
|
||||
// Bind the listener (idempotent - deInitEthApiServer() drops apiServer on a
|
||||
// W5500 reset, and this rebinds it on the restart path).
|
||||
if (!apiServer) {
|
||||
apiServer = new EthernetServer(ETH_API_PORT);
|
||||
@@ -92,7 +92,7 @@ void initEthApiServer()
|
||||
}
|
||||
// The worker is created once and kept for the lifetime of the process. It
|
||||
// idles harmlessly while apiServer is null (runOnce guards on it), so we
|
||||
// never delete it from another thread's runOnce — that would corrupt the
|
||||
// never delete it from another thread's runOnce - that would corrupt the
|
||||
// scheduler's thread list mid-iteration.
|
||||
if (!apiThread)
|
||||
apiThread = new EthApiServerThread(); // OSThread base auto-registers with the scheduler
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
/// Initialize the Ethernet HTTP API server (call after Ethernet is connected).
|
||||
/// Spawns an internal OSThread that polls accept() on a sub-second cadence,
|
||||
/// independent of the 5s Ethernet client periodic — needed because the web
|
||||
/// independent of the 5s Ethernet client periodic - needed because the web
|
||||
/// client makes many small back-to-back requests.
|
||||
void initEthApiServer();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ static constexpr const char *CERT_PATH = "/eth_cert_v2.der";
|
||||
static constexpr const char *KEY_PATH = "/eth_key_v2.der";
|
||||
static constexpr const char *IP_PATH = "/eth_cert_ip_v2.txt";
|
||||
|
||||
// Random callback for mbedtls — sources entropy from the RP2350 ROSC TRNG via
|
||||
// Random callback for mbedtls - sources entropy from the RP2350 ROSC TRNG via
|
||||
// pico-sdk get_rand_64(). Used directly as f_rng in mbedtls calls so we don't
|
||||
// have to plumb a full mbedtls_entropy_context + ctr_drbg. The hardware TRNG
|
||||
// is cryptographically suitable per pico-sdk docs (ROSC + whitening).
|
||||
@@ -175,7 +175,7 @@ static bool generateCert(IPAddress ip, EthCertMaterial &out)
|
||||
}
|
||||
|
||||
// ExtendedKeyUsage: serverAuth. NSS / Firefox refuse to treat a cert
|
||||
// as a TLS server cert without this extension since 2023 — the error
|
||||
// as a TLS server cert without this extension since 2023 - the error
|
||||
// surfaces as a non-overridable "Secure Connection Failed" with no
|
||||
// "Accept the Risk" path.
|
||||
mbedtls_asn1_sequence ekuSeq;
|
||||
@@ -293,13 +293,13 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
// current process still has the in-memory cert ready for use.
|
||||
//
|
||||
// Clear the IP commit-marker FIRST so a reset mid-write can't leave the marker
|
||||
// pointing at a half-written (or stale-paired) cert/key — the load path only
|
||||
// pointing at a half-written (or stale-paired) cert/key - the load path only
|
||||
// trusts the cache when the marker matches. Writing the marker LAST commits the
|
||||
// new pair atomically w.r.t. the loader.
|
||||
writeText(IP_PATH, "");
|
||||
if (!writeBinary(CERT_PATH, out.certDer.data(), out.certDer.size()) ||
|
||||
!writeBinary(KEY_PATH, out.keyDer.data(), out.keyDer.size()) || !writeText(IP_PATH, ipStr)) {
|
||||
LOG_WARN("ETH CERT: persist failed — will regenerate next boot");
|
||||
LOG_WARN("ETH CERT: persist failed - will regenerate next boot");
|
||||
} else {
|
||||
LOG_INFO("ETH CERT: persisted to LittleFS");
|
||||
}
|
||||
@@ -310,7 +310,7 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
// Worker that defers cert gen off the Periodic thread (which has a tight stack
|
||||
// and ticks every 5s alongside reconnect / NTP / MQTT). Waits for a non-zero IP,
|
||||
// generates/loads the cert for it, then keeps polling at CERT_RECHECK_MS so a
|
||||
// DHCP lease change to a new IP regenerates the cert — the SAN must track the
|
||||
// DHCP lease change to a new IP regenerates the cert - the SAN must track the
|
||||
// current address or browsers reject the new one. The steady-state poll is just
|
||||
// localIP() + compare; ECDSA keygen only reruns when the IP actually changes,
|
||||
// and it runs on this thread's own stack (not the Periodic's).
|
||||
@@ -337,7 +337,7 @@ class EthCertThread : public concurrency::OSThread
|
||||
// regenerates whenever its saved IP != ip, so the cert SAN follows.
|
||||
bool ok = ensureCertForIp(ip, material_);
|
||||
if (!ok) {
|
||||
LOG_ERROR("ETH CERT: pipeline FAILED — TLS server will not start");
|
||||
LOG_ERROR("ETH CERT: pipeline FAILED - TLS server will not start");
|
||||
// Don't leave isReady() reporting true with empty material: a later TLS
|
||||
// teardown (e.g. a W5500 reset) would then fail initTlsContext() and stay
|
||||
// disabled. Clear readiness so the TLS worker waits and the next poll
|
||||
|
||||
@@ -35,7 +35,7 @@ bool isEthCertReady();
|
||||
// Snapshot of the generated material once isEthCertReady(). Empty otherwise.
|
||||
const EthCertMaterial &getEthCert();
|
||||
|
||||
// Monotonic counter bumped each time the cert is (re)generated — e.g. when a DHCP
|
||||
// Monotonic counter bumped each time the cert is (re)generated - e.g. when a DHCP
|
||||
// lease change moves us to a new IP. The TLS server reloads when this changes.
|
||||
uint32_t getEthCertGeneration();
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "mesh/eth/ethTlsApiServer.h"
|
||||
#endif
|
||||
#ifdef USE_ARDUINO_ETHERNET
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5100/W5200/W5500
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet - supports W5100/W5200/W5500
|
||||
// Shorter DHCP timeout so LoRa startup isn't blocked when no DHCP server is present.
|
||||
#define ETH_DHCP_TIMEOUT_MS 10000
|
||||
#else
|
||||
@@ -181,12 +181,12 @@ static int32_t reconnectETH()
|
||||
initEthApiServer();
|
||||
#endif
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_TLS_API) && defined(ARCH_RP2040)
|
||||
// Phase 2.1-bis — cert gen runs on its own OSThread so ECDSA keygen
|
||||
// Phase 2.1-bis - cert gen runs on its own OSThread so ECDSA keygen
|
||||
// + DER encoding + LittleFS write don't share the Periodic stack
|
||||
// (which overflowed in the original inline attempt). The thread
|
||||
// polls for a non-zero IP itself and runs once.
|
||||
initEthCertThread();
|
||||
// Phase 2.2 — TLS server skeleton on TCP/443. The worker waits
|
||||
// Phase 2.2 - TLS server skeleton on TCP/443. The worker waits
|
||||
// until the cert thread signals isEthCertReady() before binding.
|
||||
initEthTlsApiServer();
|
||||
#endif
|
||||
@@ -220,7 +220,7 @@ static int32_t reconnectETH()
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
ethOTALoop();
|
||||
#endif
|
||||
// ethApiServer runs on its own OSThread (20ms ticks) — not polled here.
|
||||
// ethApiServer runs on its own OSThread (20ms ticks) - not polled here.
|
||||
|
||||
return 5000; // every 5 seconds
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ static const uint32_t OTA_AUTH_COOLDOWN_MS = 5000; // 5s cooldown after failed a
|
||||
static const size_t OTA_NONCE_SIZE = 32;
|
||||
static const size_t OTA_HASH_SIZE = 32;
|
||||
|
||||
// OTA PSK — override via USERPREFS_OTA_PSK in userPrefs.jsonc
|
||||
// OTA PSK - override via USERPREFS_OTA_PSK in userPrefs.jsonc
|
||||
// USERPREFS_OTA_PSK is stringified by PlatformIO (wrapped in quotes), so we
|
||||
// use a char[] and sizeof-1 to exclude the trailing NUL byte from the hash.
|
||||
#ifdef USERPREFS_OTA_PSK
|
||||
@@ -96,7 +96,7 @@ static void computeAuthHash(const uint8_t *nonce, size_t nonceLen, const uint8_t
|
||||
/// Challenge-response authentication. Returns true if client is authenticated.
|
||||
static bool authenticateClient(EthernetClient &client)
|
||||
{
|
||||
// Rate-limit after failed auth — close silently so the error byte is not
|
||||
// Rate-limit after failed auth - close silently so the error byte is not
|
||||
// misinterpreted as part of the nonce by a re-trying client.
|
||||
if (lastAuthFailure != 0 && (millis() - lastAuthFailure) < OTA_AUTH_COOLDOWN_MS) {
|
||||
LOG_WARN("ETH OTA: Auth cooldown active, rejecting connection");
|
||||
@@ -140,7 +140,7 @@ static bool authenticateClient(EthernetClient &client)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auth success — send ACK
|
||||
// Auth success - send ACK
|
||||
client.write(OTA_ACK);
|
||||
LOG_INFO("ETH OTA: Authentication successful");
|
||||
return true;
|
||||
@@ -179,14 +179,14 @@ static void handleOTAClient(EthernetClient &client)
|
||||
return;
|
||||
}
|
||||
|
||||
// Begin the update — this opens firmware.bin on LittleFS
|
||||
// Begin the update - this opens firmware.bin on LittleFS
|
||||
if (!Update.begin(hdr.firmwareSize)) {
|
||||
LOG_ERROR("ETH OTA: Update.begin() failed, error=%u", Update.getError());
|
||||
client.write(OTA_ERR_BEGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
// ACK the header — client can start sending firmware data
|
||||
// ACK the header - client can start sending firmware data
|
||||
client.write(OTA_ACK);
|
||||
|
||||
// Receive firmware in chunks
|
||||
@@ -252,7 +252,7 @@ static void handleOTAClient(EthernetClient &client)
|
||||
return;
|
||||
}
|
||||
|
||||
// Finalize — this calls picoOTA.commit() which stages the update for the
|
||||
// Finalize - this calls picoOTA.commit() which stages the update for the
|
||||
// bootloader
|
||||
if (!Update.end(true)) {
|
||||
LOG_ERROR("ETH OTA: Update.end() failed, error=%u", Update.getError());
|
||||
@@ -265,7 +265,7 @@ static void handleOTAClient(EthernetClient &client)
|
||||
client.flush();
|
||||
delay(500);
|
||||
|
||||
// Reboot — the built-in bootloader will apply the update from LittleFS
|
||||
// Reboot - the built-in bootloader will apply the update from LittleFS
|
||||
rp2040.reboot();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ static constexpr uint32_t MEDIUM_THRESHOLD_MS = 30000;
|
||||
static constexpr int32_t ACTIVE_INTERVAL_MS = 20;
|
||||
static constexpr int32_t MEDIUM_INTERVAL_MS = 100;
|
||||
static constexpr int32_t IDLE_INTERVAL_MS = 500;
|
||||
// Matches the keep-alive idle window in ethApiHandlers — if the handler
|
||||
// Matches the keep-alive idle window in ethApiHandlers - if the handler
|
||||
// loop calls read() and netRecv blocked for 10 s, the 3 s idle deadline
|
||||
// inside parseRequest would be irrelevant and the OSThread would stay stuck
|
||||
// long after a quiet browser closed its end of the TCP socket.
|
||||
@@ -52,7 +52,7 @@ static int picoRand(void * /*ctx*/, unsigned char *out, size_t len)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One-shot TLS context lives in BSS — keeps mbedtls allocations off the
|
||||
// One-shot TLS context lives in BSS - keeps mbedtls allocations off the
|
||||
// OSThread stack (lesson from Phase 2.1-bis: stack budget is tight on M33).
|
||||
static EthernetServer *tlsServer = nullptr;
|
||||
static mbedtls_x509_crt certChain;
|
||||
@@ -73,7 +73,7 @@ static int netSend(void *ctx, const unsigned char *buf, size_t len)
|
||||
// Block-with-yield until the W5500 TX buffer can absorb the chunk.
|
||||
// Returning WANT_WRITE without delay made mbedtls_ssl_handshake() spin
|
||||
// at ~180k iter/s when Chrome was slow to drain the socket during the
|
||||
// ECDHE-ECDSA ServerKeyExchange — the original code logged exactly that
|
||||
// ECDHE-ECDSA ServerKeyExchange - the original code logged exactly that
|
||||
// signature (ret=-0x6880 / WANT_WRITE) tight-looping forever. Firefox
|
||||
// happened to read fast enough that the buffer never filled.
|
||||
uint32_t t0 = millis();
|
||||
@@ -96,12 +96,12 @@ static int netRecv(void *ctx, unsigned char *buf, size_t len)
|
||||
// Block-with-timeout: spin until bytes arrive, the peer closes, or we
|
||||
// exceed the per-recv budget. Pure non-blocking (return WANT_READ) would
|
||||
// require mbedtls_ssl_handshake to be driven from the runOnce dispatcher
|
||||
// — overkill for the Phase 2.2 skeleton with a single in-flight session.
|
||||
// - overkill for the Phase 2.2 skeleton with a single in-flight session.
|
||||
//
|
||||
// Pet the 8 s hardware watchdog from inside the poll loop. We sit here
|
||||
// for up to RECV_TIMEOUT_MS waiting for the next keep-alive request, and
|
||||
// a quiet client can string two such waits back-to-back (6 s) plus the
|
||||
// earlier handshake/handler time — easily past the watchdog deadline.
|
||||
// earlier handshake/handler time - easily past the watchdog deadline.
|
||||
// The main loop()'s watchdog_update() never runs while the OSThread is
|
||||
// inside serveClient(), so it has to be done here.
|
||||
uint32_t t0 = millis();
|
||||
@@ -152,7 +152,7 @@ class MbedTlsStream : public IStreamReadWrite
|
||||
size_t pending = mbedtls_ssl_get_bytes_avail(ssl_);
|
||||
if (pending > 0)
|
||||
return (int)pending;
|
||||
// Best-effort: report network bytes (rough proxy — handlers usually
|
||||
// Best-effort: report network bytes (rough proxy - handlers usually
|
||||
// call read() in a loop and tolerate slow streams).
|
||||
return client_->available();
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class EthTlsApiServerThread : public concurrency::OSThread
|
||||
if (!isEthCertReady())
|
||||
return 500;
|
||||
if (!initTlsContext())
|
||||
return INT32_MAX; // hard fail — TLS server stays disabled
|
||||
return INT32_MAX; // hard fail - TLS server stays disabled
|
||||
loadedCertGen_ = getEthCertGeneration();
|
||||
tlsReady = true;
|
||||
}
|
||||
@@ -330,7 +330,7 @@ void deInitEthTlsApiServer()
|
||||
{
|
||||
// A W5500 chip reset leaves tlsServer bound to a dead socket and the cached
|
||||
// mbedTLS context stale. Reset the worker back to Phase A (free the context,
|
||||
// drop the listener, clear tlsReady) WITHOUT deleting the OSThread — its next
|
||||
// drop the listener, clear tlsReady) WITHOUT deleting the OSThread - its next
|
||||
// runOnce re-waits for isEthCertReady() and rebuilds the context + rebinds
|
||||
// TCP/443. Safe to free here: this runs in reconnectETH (ethConnect thread),
|
||||
// and the cooperative scheduler guarantees tlsThread is not mid-runOnce, so
|
||||
|
||||
@@ -184,7 +184,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
token at unlock time: the client-supplied boots_remaining when
|
||||
non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS).
|
||||
Note that boots_remaining == 0 in this message means "use firmware
|
||||
default", NOT "zero boots" — a client computing the ceiling for
|
||||
default", NOT "zero boots" - a client computing the ceiling for
|
||||
display should mirror that resolution rather than multiplying the
|
||||
raw request value.
|
||||
|
||||
@@ -194,7 +194,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
|
||||
Uses millis() (CPU uptime), not wall-clock time, so the cap is
|
||||
immune to GPS spoofing, RTC backup-battery removal, and Faraday
|
||||
cage isolation — none of those move the uptime counter. The only
|
||||
cage isolation - none of those move the uptime counter. The only
|
||||
way to reset the session clock is a reboot, which costs a boot
|
||||
from the on-flash, HMAC-bound counter. */
|
||||
uint32_t max_session_seconds;
|
||||
@@ -211,7 +211,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
|
||||
NOT reversed by this operation: APPROTECT. Once the debug port
|
||||
lockout has been burned (on silicon where it is effective) it is
|
||||
permanent — disabling lockdown decrypts your data and removes the
|
||||
permanent - disabling lockdown decrypts your data and removes the
|
||||
access gates, but the SWD/JTAG port stays locked for the life of
|
||||
the device (recoverable only via a full chip erase over a debug
|
||||
probe, which destroys all data). Clients should make this
|
||||
|
||||
@@ -332,7 +332,7 @@ typedef enum _meshtastic_CotType {
|
||||
/* y-: TAKTALK room/membership broadcast. Payload carried via the
|
||||
TakTalkRoomData typed variant (sender_callsign, room_id, room_name,
|
||||
participants). The CoT type literally has a trailing dash and no
|
||||
second atom — not a typo. */
|
||||
second atom - not a typo. */
|
||||
meshtastic_CotType_CotType_y = 126
|
||||
} meshtastic_CotType;
|
||||
|
||||
@@ -380,7 +380,7 @@ typedef enum _meshtastic_DrawnShape_Kind {
|
||||
/* u-r-b-bullseye: Bullseye ring with range rings and bearing reference */
|
||||
meshtastic_DrawnShape_Kind_Kind_Bullseye = 7,
|
||||
/* u-d-c-e: Ellipse with distinct major/minor axes (same storage as
|
||||
Kind_Circle — uses major_cm/minor_cm/angle_deg — but receivers
|
||||
Kind_Circle - uses major_cm/minor_cm/angle_deg - but receivers
|
||||
render it as a non-circular ellipse rather than a round circle). */
|
||||
meshtastic_DrawnShape_Kind_Kind_Ellipse = 8,
|
||||
/* u-d-v: 2D vehicle outline drawn on the map. Vertices carry the
|
||||
@@ -400,7 +400,7 @@ typedef enum _meshtastic_DrawnShape_Kind {
|
||||
end of parse; builder uses it to decide which of <strokeColor> /
|
||||
<fillColor> to emit in the reconstructed XML. */
|
||||
typedef enum _meshtastic_DrawnShape_StyleMode {
|
||||
/* Unspecified — receiver infers from which color fields are non-zero. */
|
||||
/* Unspecified - receiver infers from which color fields are non-zero. */
|
||||
meshtastic_DrawnShape_StyleMode_StyleMode_Unspecified = 0,
|
||||
/* Stroke only. No <fillColor> in the source XML. Used for polylines,
|
||||
ranging lines, bullseye rings. */
|
||||
@@ -417,7 +417,7 @@ typedef enum _meshtastic_DrawnShape_StyleMode {
|
||||
alone is ambiguous (e.g. a-u-G could be a 2525 symbol or a custom icon
|
||||
depending on the iconset path). */
|
||||
typedef enum _meshtastic_Marker_Kind {
|
||||
/* Unspecified — fall back to TAKPacketV2.cot_type_id */
|
||||
/* Unspecified - fall back to TAKPacketV2.cot_type_id */
|
||||
meshtastic_Marker_Kind_Kind_Unspecified = 0,
|
||||
/* b-m-p-s-m: Spot map marker */
|
||||
meshtastic_Marker_Kind_Kind_Spot = 1,
|
||||
@@ -680,10 +680,10 @@ typedef struct _meshtastic_AircraftTrack {
|
||||
hundred meters of the anchor has per-vertex deltas in the ±10^4 range.
|
||||
Under sint32+zigzag those encode as 2 bytes each (tag+varint), versus the
|
||||
4 bytes that sfixed32 would always require. At 32 vertices that is ~128
|
||||
bytes of savings — the difference between fitting under the LoRa MTU or
|
||||
bytes of savings - the difference between fitting under the LoRa MTU or
|
||||
not. Absolute coordinates (values ~10^9) would cost sint32 varint 5 bytes
|
||||
per field, which is why TAKPacketV2's top-level latitude_i / longitude_i
|
||||
stay sfixed32 — only small values win with sint32. */
|
||||
stay sfixed32 - only small values win with sint32. */
|
||||
typedef struct _meshtastic_CotGeoPoint {
|
||||
/* Latitude delta from TAKPacketV2.latitude_i, in 1e-7 degree units.
|
||||
Add to the enclosing event's latitude_i to recover the absolute latitude. */
|
||||
@@ -791,7 +791,7 @@ typedef struct _meshtastic_Marker {
|
||||
|
||||
Covers CoT type u-rb-a. The anchor position is on
|
||||
TAKPacketV2.latitude_i/longitude_i; the target endpoint is carried as a
|
||||
CotGeoPoint — same delta-from-anchor encoding used by DrawnShape.vertices
|
||||
CotGeoPoint - same delta-from-anchor encoding used by DrawnShape.vertices
|
||||
so a self-anchored RAB (common case) encodes in zero bytes. */
|
||||
typedef struct _meshtastic_RangeAndBearing {
|
||||
/* Target/anchor endpoint (delta-encoded from TAKPacketV2.latitude_i/longitude_i). */
|
||||
@@ -899,12 +899,12 @@ typedef struct _meshtastic_CasevacReport {
|
||||
same as the envelope callsign but ATAK sometimes carries a distinct
|
||||
ops-number here. */
|
||||
pb_callback_t title;
|
||||
/* Primary medline free-text — the single most clinically important line
|
||||
/* Primary medline free-text - the single most clinically important line
|
||||
on a MEDLINE form (e.g. "2 urgent litter patients, smoke on approach").
|
||||
MUST be preserved under MTU pressure as long as any casevac is sent. */
|
||||
pb_callback_t medline_remarks;
|
||||
/* Line 3 (newer ATAK format): patient counts by precedence level.
|
||||
Coexists with the enum-style `precedence` field (tag 1) — older ATAK
|
||||
Coexists with the enum-style `precedence` field (tag 1) - older ATAK
|
||||
emits a single enum, newer ATAK emits these counts, and both can be
|
||||
set simultaneously. Senders populate whichever style(s) the source
|
||||
XML had; receivers prefer counts when non-zero. */
|
||||
@@ -946,19 +946,19 @@ typedef struct _meshtastic_CasevacReport {
|
||||
(e.g. "Primary HLZ is soccer field"). */
|
||||
pb_callback_t hlz_remarks;
|
||||
/* Per-patient clinical records. Each entry is one patient's ZMIST card
|
||||
(Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable —
|
||||
(Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable -
|
||||
a mass-casualty event can carry 1-6 entries in practice, limited by
|
||||
the 237 B LoRa MTU. */
|
||||
pb_callback_t zmist;
|
||||
} meshtastic_CasevacReport;
|
||||
|
||||
/* Per-patient clinical summary record — one entry per patient in a CASEVAC.
|
||||
/* Per-patient clinical summary record - one entry per patient in a CASEVAC.
|
||||
Maps directly to ATAK's <zMist> child element inside <zMistsMap>.
|
||||
All fields are optional free-text; senders populate what they have. */
|
||||
typedef struct _meshtastic_ZMistEntry {
|
||||
/* Patient identifier / sequence label (e.g. "ZMIST-1", "ZMIST-2"). */
|
||||
pb_callback_t title;
|
||||
/* Zap number — unique patient tracking ID (often a terse code like
|
||||
/* Zap number - unique patient tracking ID (often a terse code like
|
||||
"Gunshot" or a serial). */
|
||||
pb_callback_t z;
|
||||
/* Mechanism of injury (e.g. "Penetrating trauma", "Blast injury"). */
|
||||
@@ -997,7 +997,7 @@ typedef struct _meshtastic_EmergencyAlert {
|
||||
creation time; the fields below carry structured metadata the raw-detail
|
||||
fallback currently loses.
|
||||
|
||||
Fields are deliberately lean — this variant is closer to the MTU ceiling
|
||||
Fields are deliberately lean - this variant is closer to the MTU ceiling
|
||||
than the others, so every string is capped in options. */
|
||||
typedef struct _meshtastic_TaskRequest {
|
||||
/* Short tag for the task category (e.g. "engage", "observe", "recon",
|
||||
@@ -1017,7 +1017,7 @@ typedef struct _meshtastic_TaskRequest {
|
||||
|
||||
/* Weather annotation from <environment> CoT detail element.
|
||||
|
||||
Attaches to any TAKPacketV2 regardless of payload_variant — an Aircraft,
|
||||
Attaches to any TAKPacketV2 regardless of payload_variant - an Aircraft,
|
||||
PLI, or Marker can all carry observed conditions at the emitting station.
|
||||
ATAK-CIV ships an XSD for <environment> but no dedicated handler, so the
|
||||
element round-trips through the generic detail pipeline; this message
|
||||
@@ -1026,7 +1026,7 @@ typedef struct _meshtastic_TaskRequest {
|
||||
Target wire cost: ~6-8 bytes compressed with a fully populated instance.
|
||||
|
||||
Named `TAKEnvironment` (not just `Environment`) because the bare name
|
||||
collides with `SwiftUI.Environment` — every SwiftUI view in a consuming
|
||||
collides with `SwiftUI.Environment` - every SwiftUI view in a consuming
|
||||
iOS app uses the `@Environment` property wrapper, and importing the
|
||||
generated proto module would make `Environment` ambiguous in every one
|
||||
of those files. The `TAK` prefix matches the convention used by the
|
||||
@@ -1055,7 +1055,7 @@ typedef struct _meshtastic_TAKEnvironment {
|
||||
The receiving ATAK client restores those from its own defaults, same as
|
||||
every other CoT carried over Meshtastic today.
|
||||
|
||||
Attaches to any TAKPacketV2 — a PLI with a sensor on the operator's head,
|
||||
Attaches to any TAKPacketV2 - a PLI with a sensor on the operator's head,
|
||||
an Aircraft with a FLIR turret, a Marker dropped on a UAV.
|
||||
Target wire cost: ~7-14 bytes compressed (dominated by model string). */
|
||||
typedef struct _meshtastic_SensorFov {
|
||||
@@ -1065,30 +1065,30 @@ typedef struct _meshtastic_SensorFov {
|
||||
SensorDetailHandler default (270°) and save varint bytes over centi-deg. */
|
||||
uint32_t azimuth_deg;
|
||||
/* Maximum range of the cone in meters.
|
||||
Optional — if unset, receivers should use the ATAK-CIV default of 100m. */
|
||||
Optional - if unset, receivers should use the ATAK-CIV default of 100m. */
|
||||
bool has_range_m;
|
||||
uint32_t range_m;
|
||||
/* Horizontal field of view in whole degrees (cone's angular width).
|
||||
ATAK-CIV default is 45°. */
|
||||
uint32_t fov_horizontal_deg;
|
||||
/* Vertical field of view in whole degrees. ATAK-CIV default is 45°.
|
||||
Optional — a value of 0 means "not set / use horizontal FOV". */
|
||||
Optional - a value of 0 means "not set / use horizontal FOV". */
|
||||
uint32_t fov_vertical_deg;
|
||||
/* Elevation angle in whole degrees. Positive = up, negative = down.
|
||||
Range -90 to +90. sint32 for varint efficiency on small negatives. */
|
||||
int32_t elevation_deg;
|
||||
/* Roll (camera tilt) in whole degrees, -180 to +180.
|
||||
Optional — use 0 if the sensor doesn't track roll. */
|
||||
Optional - use 0 if the sensor doesn't track roll. */
|
||||
int32_t roll_deg;
|
||||
/* Free-form device model identifier, e.g. "FLIR-Boson-640", "SEEK".
|
||||
Optional — empty string means "unknown model" (ATAK-CIV default). */
|
||||
Optional - empty string means "unknown model" (ATAK-CIV default). */
|
||||
pb_callback_t model;
|
||||
} meshtastic_SensorFov;
|
||||
|
||||
/* TAKTALK chat message payload (CoT type m-t-t).
|
||||
|
||||
TAKTALK is an ATAK plugin for voice + text team messaging. The voice
|
||||
audio stream goes over UDP/RTP and is NOT carried by the mesh — only
|
||||
audio stream goes over UDP/RTP and is NOT carried by the mesh - only
|
||||
the text envelope (this message) is. `from_voice` marks messages sent
|
||||
via push-to-talk speech-to-text so receivers can render a mic icon
|
||||
next to the text.
|
||||
@@ -1122,7 +1122,7 @@ typedef struct _meshtastic_TakTalkMessage {
|
||||
Announces a TAKTALK chatroom's friendly name and roster so peers can
|
||||
resolve room UUIDs (used in TakTalkMessage.chatroom_id and
|
||||
GeoChat.room_id) to a display name and participant list. Not a chat
|
||||
message itself — these events are emitted by TAKTALK when rooms are
|
||||
message itself - these events are emitted by TAKTALK when rooms are
|
||||
created or memberships change. */
|
||||
typedef struct _meshtastic_TakTalkRoomData {
|
||||
/* Callsign of the device broadcasting the room state (typically the
|
||||
@@ -1161,7 +1161,7 @@ typedef struct _meshtastic_Marti {
|
||||
primary-vs-cc distinction the same way ATAK does.
|
||||
|
||||
If dest_callsign is [TAKPacketV2.callsign] (self-addressed, unusual but
|
||||
legal — e.g. ATAK echoing back to its own room), the builder still emits
|
||||
legal - e.g. ATAK echoing back to its own room), the builder still emits
|
||||
the element so loopback shapes round-trip cleanly. */
|
||||
pb_callback_t dest_callsign;
|
||||
} meshtastic_Marti;
|
||||
|
||||
@@ -1244,15 +1244,15 @@ typedef struct _meshtastic_LockdownStatus {
|
||||
/* Current lockdown state being reported. */
|
||||
meshtastic_LockdownStatus_State state;
|
||||
/* For LOCKED: machine-readable reason. Known values:
|
||||
"needs_auth" — storage already unlocked, client must auth
|
||||
"token_missing" — no boot token on flash
|
||||
"token_expired" — boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" — boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" — token tampered or wrong device
|
||||
"token_dek_fail" — token DEK decrypt failed
|
||||
"token_wrong_size" — token file corrupted
|
||||
"token_bad_magic" — token file corrupted
|
||||
"not_provisioned" — should generally use NEEDS_PROVISION state instead
|
||||
"needs_auth" - storage already unlocked, client must auth
|
||||
"token_missing" - no boot token on flash
|
||||
"token_expired" - boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" - boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" - token tampered or wrong device
|
||||
"token_dek_fail" - token DEK decrypt failed
|
||||
"token_wrong_size" - token file corrupted
|
||||
"token_bad_magic" - token file corrupted
|
||||
"not_provisioned" - should generally use NEEDS_PROVISION state instead
|
||||
Other values may be added; clients should treat unknown values as
|
||||
"locked, ask for passphrase". */
|
||||
char lock_reason[32];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/* Payload for MESH_BEACON_APP packets.
|
||||
Periodically broadcast by nodes in beacon mode.
|
||||
Listeners deliver the text message to the local inbox and cache any offered
|
||||
channel/preset for the client app to act on — the firmware never auto-applies them. */
|
||||
channel/preset for the client app to act on - the firmware never auto-applies them. */
|
||||
typedef struct _meshtastic_MeshBeacon {
|
||||
/* Human-readable beacon message. Max 100 bytes enforced by firmware on send. */
|
||||
char message[101];
|
||||
|
||||
@@ -497,7 +497,7 @@ typedef struct _meshtastic_ModuleConfig_MeshBeaconConfig {
|
||||
/* Single-target TX channel: channel settings (name + PSK) to send beacons on.
|
||||
If unset, beacons go out on the primary channel. Used only when broadcast_targets is empty.
|
||||
NOTE: the single-target path embeds the ChannelSettings inline here, whereas a
|
||||
broadcast_targets entry references a channel-table slot by channel_index instead — see
|
||||
broadcast_targets entry references a channel-table slot by channel_index instead - see
|
||||
BroadcastTarget. The two paths are equal, first-class options; only this representation differs. */
|
||||
bool has_broadcast_on_channel;
|
||||
meshtastic_ChannelSettings broadcast_on_channel;
|
||||
@@ -514,7 +514,7 @@ typedef struct _meshtastic_ModuleConfig_MeshBeaconConfig {
|
||||
each temporarily switching the radio to that entry's preset/region/channel.
|
||||
When empty, the broadcaster uses the scalar broadcast_on_preset / broadcast_on_region /
|
||||
broadcast_on_channel fields instead (the single-target path).
|
||||
Single- and multi-target are equal, first-class options — neither is preferred or
|
||||
Single- and multi-target are equal, first-class options - neither is preferred or
|
||||
deprecated. They differ only in how the TX channel is named: broadcast_on_channel embeds a
|
||||
ChannelSettings inline, while a target references an existing channel-table slot by
|
||||
channel_index (see BroadcastTarget). */
|
||||
|
||||
@@ -125,7 +125,7 @@ static inline int get_max_num_nodes()
|
||||
#elif defined(NRF52840_XXAA)
|
||||
// Keyed on the NRF52840_XXAA build flag, not ARCH_NRF52: the latter (from
|
||||
// architecture.h via configuration.h) isn't defined this early in every include
|
||||
// chain. Backed by the raw-flash ring below LittleFS — see WarmNodeStore.h.
|
||||
// chain. Backed by the raw-flash ring below LittleFS - see WarmNodeStore.h.
|
||||
#define WARM_NODE_COUNT 200
|
||||
#elif (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO)
|
||||
#define WARM_NODE_COUNT 2000 // PSRAM-equipped ESP32-S3 / native host; warm cache in PSRAM (~80 KB)
|
||||
@@ -137,7 +137,7 @@ static inline int get_max_num_nodes()
|
||||
#define WARM_NODE_COUNT 150 // RP2040 (264 KB) / RP2350 (520 KB): bounded so warm.dat write fits the 8s watchdog (#10746)
|
||||
#else
|
||||
// nRF52840 is handled explicitly above (200, raw-flash ring). Any other nRF52 (non-XXAA) and any
|
||||
// future non-ESP32/non-RP LittleFS part fall through to this 320 default — flag for review if such a
|
||||
// future non-ESP32/non-RP LittleFS part fall through to this 320 default - flag for review if such a
|
||||
// RAM-constrained nRF52 target is ever added.
|
||||
#define WARM_NODE_COUNT 320 // other LittleFS-backed parts (e.g. non-nRF52840 nRF52)
|
||||
#endif // platform
|
||||
@@ -178,7 +178,7 @@ static inline int get_max_num_nodes()
|
||||
#elif defined(ARCH_ESP32)
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 400 // classic ESP32 / S2 / C3: tightest free heap, ~4 KB (#10705)
|
||||
#else
|
||||
// nRF52 (incl. nRF52840) and RP2040/RP2350 fall through here — there is no nRF/RP branch above,
|
||||
// nRF52 (incl. nRF52840) and RP2040/RP2350 fall through here - there is no nRF/RP branch above,
|
||||
// by design. These parts have no ESP32-style WiFi+BLE coexistence eating the heap, so the larger
|
||||
// 1000-entry (~10 KB) cache fits: nRF52840 is BLE-only on 256 KB RAM; RP2040/RP2350 have 264/520 KB.
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 // nRF52 / RP2040 / RP2350 / other non-ESP32
|
||||
|
||||
@@ -46,7 +46,7 @@ Author: Marc Philipp Hammermann
|
||||
mail: marchammermann@googlemail.com
|
||||
|
||||
*/
|
||||
// Mirrors the guard in PiWebServer.h — see comment there. macOS Homebrew
|
||||
// Mirrors the guard in PiWebServer.h - see comment there. macOS Homebrew
|
||||
// provides ulfius + deps; Linux pulls them via apt. Either way, this
|
||||
// translation unit only compiles when the headers are present.
|
||||
#ifdef ARCH_PORTDUINO
|
||||
|
||||
@@ -72,7 +72,7 @@ class UdpMulticastHandler final
|
||||
LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength);
|
||||
bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp);
|
||||
if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
|
||||
// Drop packets with spoofed local origin — no legitimate LAN node should send from=0 or our own nodeNum
|
||||
// Drop packets with spoofed local origin - no legitimate LAN node should send from=0 or our own nodeNum
|
||||
if (isFromUs(&mp)) {
|
||||
LOG_WARN("UDP packet with spoofed local from=0x%08x, dropping", mp.from);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user