ota: configurable hop limit (ota config hops) + accept-gate + forward RAM guard
Bound OTA-over-LoRa duty cycle across repeaters with one runtime-tunable, persisted limit (OtaManager::max_hops, `ota config hops <0..8>`, default 3): - Accept-gate: a node ignores OTA that arrived from more than max_hops hops away (neither processes nor relays it). 0 = direct only. - Forward-cap: relay a flood only while still under max_hops, appending this node's path-hash (hop count increments like the mesh flood routing). - RAM guard: relay an OTA flood only while more than OTA_FWD_MIN_FREE packet- pool slots stay free, so heavy OTA (best-effort, lowest-priority) can never monopolise the shared pool and starve real traffic — a dropped relay is re-requested by the source. Persisted in NodePrefs (CommonCLI) and shown in `ota config`. Docs updated.
This commit is contained in:
1 parent
9ff1c883c2
commit
259802b706
8 files changed
+59
-15
No files matched your search
@@ -304,6 +304,11 @@ Message types:
|
||||
- **Relay:** replies are flooded, so transparent relay needs no per-requester addressing, and the transfer
|
||||
is trustless (the fetcher verifies every block against the signed root). Any neighbor may serve any
|
||||
fragment it has.
|
||||
- **Hop limit + duty cycle:** OTA floods accumulate one path-hash per relay (the mesh's flood routing). A
|
||||
node *accepts* a packet only if it arrived within `ota config hops` hops (default 3; `0` = direct only)
|
||||
and *relays* it only while still under that limit, appending its own hash. Relays are lowest-priority and
|
||||
are skipped when the packet pool runs low (the source retries), so heavy OTA can never monopolise a
|
||||
repeater's RAM or starve real traffic.
|
||||
|
||||
### 8.1 Two-tier discovery
|
||||
|
||||
|
||||
@@ -132,6 +132,9 @@ ota config autoinstall off # never auto-install (default)
|
||||
ota config advert 1440 # re-advertise this node every N minutes (default 1440 = 24h)
|
||||
ota config advert 0 # disable periodic re-advertise (still advertises briefly at boot)
|
||||
|
||||
ota config hops 3 # how far OTA travels: accept from / relay up to N repeater hops (default 3)
|
||||
ota config hops 0 # only exchange OTA with directly-connected nodes (never relay)
|
||||
|
||||
ota config # show the current settings
|
||||
```
|
||||
|
||||
|
||||
+16
-6
@@ -31,6 +31,10 @@ void Mesh::otaSendAdapter(void* ctx, const uint8_t* msg, uint16_t len, bool /*fl
|
||||
Packet* p = m->createOtaPacket(msg, len);
|
||||
if (p) m->sendOtaFlood(p);
|
||||
}
|
||||
|
||||
// Runtime OTA flood reach (`ota config hops`, persisted in NodePrefs): accept packets up to N hops away and
|
||||
// relay those still under N hops. 0 = direct only. Overridable per-role by subclassing.
|
||||
uint8_t Mesh::getOtaHopLimit() const { return ota::ota_ctx().manager.max_hops(); }
|
||||
#endif
|
||||
|
||||
void Mesh::begin() {
|
||||
@@ -423,21 +427,27 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
|
||||
#if defined(ENABLE_OTA)
|
||||
case PAYLOAD_TYPE_OTA: {
|
||||
// ALWAYS process every received copy: OTA handlers are idempotent, and "eventually reliable"
|
||||
// retries deliberately re-send IDENTICAL requests — if we gated processing on hasSeen(), the
|
||||
// dedup would suppress those retries and the transfer could never recover from a lost reply.
|
||||
// hasSeen() is used ONLY to avoid re-flooding the same packet more than once.
|
||||
uint8_t n = pkt->getPathHashCount(); // hops travelled to reach us (flood path-hash count)
|
||||
// Accept-gate (duty-cycle horizon): ignore OTA from further than our hop limit — neither process nor
|
||||
// relay it. 0 = only directly-received OTA. Runtime-tunable via `ota config hops`.
|
||||
if (n > getOtaHopLimit()) break;
|
||||
// ALWAYS process every accepted copy: OTA handlers are idempotent, and "eventually reliable" retries
|
||||
// deliberately re-send IDENTICAL requests — if we gated processing on hasSeen(), the dedup would
|
||||
// suppress those retries and the transfer could never recover from a lost reply. hasSeen() is used
|
||||
// ONLY to avoid re-flooding the same packet more than once.
|
||||
bool seen = _tables->hasSeen(pkt);
|
||||
ota::ota_ctx().manager.set_clock(_ms->getMillis()); // discovery jitter/ages
|
||||
ota::ota_ctx().manager.on_message(pkt->payload, pkt->payload_len); // central OTA receive (beacon/query/
|
||||
// have/manifest/data/proof; all roles)
|
||||
ota::ota_ctx().track_session(ota::ota_ctx().manager.fetchState(), _ms->getMillis());
|
||||
onOtaRecv(pkt); // optional per-example hook
|
||||
// Re-flood with a hop cap and the LOWEST priority, so OTA never competes with mesh traffic.
|
||||
uint8_t n = pkt->getPathHashCount();
|
||||
// Re-flood at the LOWEST priority and only while still under the hop limit, so OTA never competes with
|
||||
// mesh traffic. The free-pool guard keeps heavy OTA from monopolising the shared packet pool — dropping
|
||||
// a relay is safe (OTA is best-effort; the source retries).
|
||||
if (!seen && pkt->isRouteFlood() && !pkt->isMarkedDoNotRetransmit()
|
||||
&& n < getOtaHopLimit()
|
||||
&& (n + 1) * pkt->getPathHashSize() <= MAX_PATH_SIZE
|
||||
&& _mgr->getFreeCount() > OTA_FWD_MIN_FREE
|
||||
&& allowPacketForward(pkt)) {
|
||||
self_id.copyHashTo(&pkt->path[n * pkt->getPathHashSize()], pkt->getPathHashSize());
|
||||
pkt->setPathHashCount(n + 1);
|
||||
|
||||
+8
-5
@@ -3,12 +3,15 @@
|
||||
#include <Dispatcher.h>
|
||||
|
||||
#if defined(ENABLE_OTA)
|
||||
// OTA-over-LoRa: lowest TX priority (selected only after all real traffic) + default hop cap.
|
||||
// OTA-over-LoRa: lowest TX priority (selected only after all real traffic). The hop limit is runtime-
|
||||
// tunable (OtaManager::max_hops(), `ota config hops`); the default lives in OtaManager.h.
|
||||
#ifndef OTA_TX_PRIORITY
|
||||
#define OTA_TX_PRIORITY 250
|
||||
#endif
|
||||
#ifndef OTA_HOP_LIMIT_DEFAULT
|
||||
#define OTA_HOP_LIMIT_DEFAULT 3
|
||||
// An OTA flood is relayed only while this many pool slots stay free, so heavy OTA (best-effort, low-
|
||||
// priority) can never monopolise the shared packet pool and starve real traffic.
|
||||
#ifndef OTA_FWD_MIN_FREE
|
||||
#define OTA_FWD_MIN_FREE 4
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -161,8 +164,8 @@ protected:
|
||||
*/
|
||||
virtual void onOtaRecv(Packet* packet) { }
|
||||
|
||||
/** \returns the max hop count for forwarding OTA flood packets (default 3). */
|
||||
virtual uint8_t getOtaHopLimit() const { return OTA_HOP_LIMIT_DEFAULT; }
|
||||
/** \returns max OTA flood reach in hops — accept up to N, relay while < N (0=direct). `ota config hops`. */
|
||||
virtual uint8_t getOtaHopLimit() const;
|
||||
|
||||
// OTA mesh-integration is centralized in Mesh::begin()/loop()/dispatch, so every role (repeater,
|
||||
// companion, room, sensor, ...) gets fetch/serve/apply without per-example wiring.
|
||||
|
||||
@@ -53,6 +53,7 @@ void CommonCLI::syncOtaConfigFromPrefs() {
|
||||
c.manager.set_autofetch(_prefs->ota_autofetch);
|
||||
c.manager.set_checkpoint_blocks(_prefs->ota_checkpoint_blocks);
|
||||
c.manager.set_advert_mins(_prefs->ota_advert_interval);
|
||||
c.manager.set_max_hops(_prefs->ota_max_hops);
|
||||
c.autoinstall = _prefs->ota_autoinstall;
|
||||
c.allow.clear();
|
||||
for (uint8_t i = 0; i < _prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++)
|
||||
@@ -121,13 +122,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->ota_autofetch = 0; _prefs->ota_autoinstall = 0; _prefs->ota_signer_count = 0;
|
||||
_prefs->ota_checkpoint_blocks = 4; // = OTA_CHECKPOINT_BLOCKS; older prefs lack it -> stays at default
|
||||
_prefs->ota_advert_interval = 1440; // = OTA_ADVERT_INTERVAL_MINS (24h); older prefs lack it -> stays default
|
||||
_prefs->ota_max_hops = 3; // = OTA_HOP_LIMIT_DEFAULT; older prefs lack it -> stays default
|
||||
file.read((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 295
|
||||
file.read((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 296
|
||||
file.read((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 297
|
||||
file.read((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298
|
||||
file.read((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426
|
||||
file.read((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428
|
||||
// next: 430
|
||||
file.read((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 430
|
||||
// next: 431
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
|
||||
@@ -163,6 +166,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->ota_autoinstall = constrain(_prefs->ota_autoinstall, 0, 1);
|
||||
if (_prefs->ota_checkpoint_blocks > 4096) _prefs->ota_checkpoint_blocks = 4; // 0=never; cap absurd
|
||||
if (_prefs->ota_advert_interval > 10080) _prefs->ota_advert_interval = 1440; // 0=off; cap at 7 days
|
||||
if (_prefs->ota_max_hops > 8) _prefs->ota_max_hops = 3; // 0=direct only; cap absurd reach
|
||||
if (_prefs->ota_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys
|
||||
|
||||
file.close();
|
||||
@@ -235,7 +239,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298
|
||||
file.write((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426
|
||||
file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428
|
||||
// next: 430
|
||||
file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 430
|
||||
// next: 431
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -365,6 +370,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
_prefs->ota_autofetch = c.manager.autofetch();
|
||||
_prefs->ota_checkpoint_blocks = c.manager.checkpoint_blocks();
|
||||
_prefs->ota_advert_interval = c.manager.advert_mins();
|
||||
_prefs->ota_max_hops = c.manager.max_hops();
|
||||
_prefs->ota_autoinstall = c.autoinstall;
|
||||
_prefs->ota_signer_count = c.allow.count();
|
||||
for (uint8_t i = 0; i < c.allow.count() && i < MAX_OTA_SIGNERS; i++)
|
||||
|
||||
@@ -72,6 +72,7 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t ota_signers[4][32]; // trusted Ed25519 signer pubkeys (== MAX_OTA_SIGNERS)
|
||||
uint16_t ota_checkpoint_blocks; // resume checkpoint cadence (blocks); 0=never. Default 4 (runtime-tunable)
|
||||
uint16_t ota_advert_interval; // OTA beacon re-advertise cadence (mins); 0=off. Default 1440 (24h, tunable)
|
||||
uint8_t ota_max_hops; // OTA flood reach in hops; 0=direct only. Default 3 (runtime-tunable)
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
|
||||
@@ -277,12 +277,18 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (m < 0 || m > 10080) { strcpy(reply, "ERR usage: ota config advert <0..10080> (minutes; 0=disable)"); return true; }
|
||||
c.manager.set_advert_mins((uint16_t)m); c.config_dirty = true;
|
||||
sprintf(reply, "OK re-advertise every %ld min (saved)%s", m, m == 0 ? " — periodic advert disabled" : "");
|
||||
} else if (strncmp(p, "hops ", 5) == 0) { // OTA flood reach in hops (0 = direct only)
|
||||
long h = atol(p + 5);
|
||||
if (h < 0 || h > 8) { strcpy(reply, "ERR usage: ota config hops <0..8> (hops; 0 = direct only)"); return true; }
|
||||
c.manager.set_max_hops((uint8_t)h); c.config_dirty = true;
|
||||
sprintf(reply, "OK OTA reach = %ld hop%s (saved)%s", h, h == 1 ? "" : "s", h == 0 ? " — direct only" : "");
|
||||
} else { // show current policy
|
||||
uint8_t af = c.manager.autofetch();
|
||||
sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u advert=%umin keys=%u (persisted)",
|
||||
sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u advert=%umin hops=%u keys=%u (persisted)",
|
||||
af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off",
|
||||
c.autoinstall == OtaContext::AUTOINSTALL_TRUSTED ? "trusted" : "off",
|
||||
(unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(), (unsigned)c.allow.count());
|
||||
(unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(),
|
||||
(unsigned)c.manager.max_hops(), (unsigned)c.allow.count());
|
||||
}
|
||||
|
||||
// ---- trusted signer allowlist (security config; persisted): `ota key add|rm <hex>` / `ota key` lists ----
|
||||
|
||||
@@ -40,6 +40,9 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len)
|
||||
#ifndef OTA_ADVERT_INTERVAL_MINS
|
||||
#define OTA_ADVERT_INTERVAL_MINS 1440 // re-advertise our served set every N minutes after the boot burst (24h)
|
||||
#endif
|
||||
#ifndef OTA_HOP_LIMIT_DEFAULT
|
||||
#define OTA_HOP_LIMIT_DEFAULT 3 // default OTA flood reach in hops: accept <= N, relay while < N (0=direct)
|
||||
#endif
|
||||
#ifndef OTA_MF_FRAG
|
||||
#define OTA_MF_FRAG 176 // manifest bytes per OTA_MANIFEST fragment (<= MAX_PACKET_PAYLOAD - header)
|
||||
#endif
|
||||
@@ -213,6 +216,12 @@ public:
|
||||
// (boot burst only). Default OTA_ADVERT_INTERVAL_MINS (24h).
|
||||
void set_advert_mins(uint16_t m) { _advert_mins = m; }
|
||||
uint16_t advert_mins() const { return _advert_mins; }
|
||||
|
||||
// Max OTA flood reach in HOPS (runtime-tunable, persisted): a node accepts OTA from up to N hops away and
|
||||
// relays packets still under N hops; 0 = direct only (accept path_count 0, never relay). Bounds duty-cycle
|
||||
// when crossing repeaters. Default OTA_HOP_LIMIT_DEFAULT. Set via `ota config hops`.
|
||||
void set_max_hops(uint8_t h) { _max_hops = h; }
|
||||
uint8_t max_hops() const { return _max_hops; }
|
||||
bool fetched_is_signed() const { return (_fflags & MFLAG_SIGNED) != 0; } // flags of the fetched manifest
|
||||
|
||||
// This node's id (pubkey[0:4]), stamped into adverts we send so receivers can count distinct seeders.
|
||||
@@ -335,6 +344,7 @@ private:
|
||||
uint8_t _autofetch = AUTOFETCH_OFF; // auto-fetch policy (persisted in NodePrefs)
|
||||
uint16_t _checkpoint_blocks = OTA_CHECKPOINT_BLOCKS; // resume checkpoint cadence (persisted)
|
||||
uint16_t _advert_mins = OTA_ADVERT_INTERVAL_MINS; // beacon re-advertise cadence, minutes; 0=off (persisted)
|
||||
uint8_t _max_hops = OTA_HOP_LIMIT_DEFAULT; // OTA flood reach in hops; 0=direct only (persisted)
|
||||
uint8_t _fflags = 0; // flags of the manifest currently being fetched
|
||||
// multi-fragment reassembly of the current block (per-block 2-phase: fetch data, then its proof)
|
||||
uint32_t _reasm_block = NO_BLOCK; // block being reassembled / awaiting proof (none)
|
||||
|
||||
Reference in new issue
Block a user