mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
feat(sim): sim_radio_get_params export + fix identity-collision RNG bug
meshcore-solo-site's new cross-visitor relay bridge (Phase 3) needs a way
to read a sim instance's live freq/bw/sf/cr so a WebSocket relay server
can fan traffic out only to other visitors tuned to the same params,
mirroring real LoRa channel isolation. Added sim_radio_get_params()
(examples/companion_radio/main.cpp), reading NodePrefs directly -- the
real live source of truth, since SimRadio::setParams() has always been a
complete no-op (accepts and discards its arguments). Out-params via
pointer, same buffer-pointer idiom sim_radio_poll_tx() already uses;
build_wasm.sh needed HEAPF32/HEAP32 added to EXPORTED_RUNTIME_METHODS so
JS can read them back.
While testing the relay bridge with two independent browser contexts, hit
a real bug: sim_instance_salt() (SimInstance.h) is a pure function of the
simInstanceTag STRING ('hero'/'B'/'R'), so it's only useful for telling
apart same-tab instances -- it's identical for two genuinely different
visitors who both boot a 'hero' instance. Combined with SimRNG::begin()'s
other seed ingredients -- time(NULL) (1-second resolution) and a WASM
heap pointer (no ASLR inside the sandbox, so it's fully deterministic
across independent boots of the same binary) -- two different visitors
landing on the same wall-clock second reliably generated byte-identical
Ed25519 identities (confirmed: two fresh browser contexts launched
together produced provably identical advert packets end to end).
Fixed by mixing in real crypto.getRandomValues()-sourced entropy from the
host page (new sim_instance_entropy(), read the same way simInstanceTag
already is) into both SimRNG::begin() and the twin seeding pattern in
SimRadio::getRngSeed(). Applies to every sim instance uniformly (hero/B/R
all get proper per-session entropy now), not just the new relay path --
strictly an improvement with no compatibility concern, since identity
generation only ever runs once per fresh/empty IDBFS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iubftDmKNWmkNnhJRz8UH
This commit is contained in:
@@ -430,6 +430,22 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_get_num_contacts() {
|
|||||||
return the_mesh.getNumContacts();
|
return the_mesh.getNumContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The live radio channel config -- lets a host page (meshcore-solo-site's
|
||||||
|
// WebSocket relay bridge) tag this instance's outgoing packets with what
|
||||||
|
// it's currently tuned to, so a server-side relay can fan bytes out only to
|
||||||
|
// other instances tuned to the same freq/bw/sf/cr, mirroring real LoRa
|
||||||
|
// channel isolation. Reads NodePrefs directly (not SimRadio, which never
|
||||||
|
// stores the values SimRadio::setParams() is called with) since NodePrefs
|
||||||
|
// is the actual live source of truth -- the same fields the on-device
|
||||||
|
// Settings > Radio screen edits via UITask::applyRadioParams().
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE void sim_radio_get_params(float* out_freq, float* out_bw, int* out_sf, int* out_cr) {
|
||||||
|
NodePrefs* p = g_sim_ready ? the_mesh.getNodePrefs() : nullptr;
|
||||||
|
*out_freq = p ? p->freq : 0;
|
||||||
|
*out_bw = p ? p->bw : 0;
|
||||||
|
*out_sf = p ? p->sf : 0;
|
||||||
|
*out_cr = p ? p->cr : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Real hardware ships with NodePrefs::HP_DEFAULT -- a curated 5-page Home
|
// Real hardware ships with NodePrefs::HP_DEFAULT -- a curated 5-page Home
|
||||||
// carousel (Clock/Tools/Shutdown/Favourites/Map) -- so a first-time user
|
// carousel (Clock/Tools/Shutdown/Favourites/Map) -- so a first-time user
|
||||||
// isn't handed 13 pages to joystick through; the rest (Recent/Radio/
|
// isn't handed 13 pages to joystick through; the rest (Recent/Radio/
|
||||||
|
|||||||
@@ -51,6 +51,35 @@ inline uint32_t sim_instance_salt() {
|
|||||||
return h >>> 0;
|
return h >>> 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// meshcore-solo-site's WebSocket relay bridge (Phase 3) exposed a real gap
|
||||||
|
// in sim_instance_salt() alone: it's a pure function of the simInstanceTag
|
||||||
|
// STRING ('hero'/'B'/'R'), so it's only ever useful for telling apart
|
||||||
|
// same-tab instances that use different tags -- it's the SAME value every
|
||||||
|
// time for two genuinely different browser tabs/machines that both boot a
|
||||||
|
// 'hero' instance, which is exactly the new cross-visitor case. Combined
|
||||||
|
// with SimRNG::begin()'s other two seed ingredients -- time(NULL) (1-second
|
||||||
|
// resolution: two real visitors loading the page in the same second collide
|
||||||
|
// outright) and `(uintptr_t)this` (a WASM linear-memory address, which is
|
||||||
|
// fully deterministic across independent boots of the same binary doing the
|
||||||
|
// same allocation sequence -- there's no ASLR inside a wasm sandbox, so this
|
||||||
|
// contributes zero actual entropy, not "usually" different) -- two distinct
|
||||||
|
// real visitors landing on the exact same wall-clock second reliably
|
||||||
|
// produced byte-identical generated Ed25519 identities (confirmed while
|
||||||
|
// testing the relay bridge: two independent, freshly-IDBFS browser contexts
|
||||||
|
// launched together produced provably identical advert packets end to end).
|
||||||
|
// A host page now passes one genuinely random value from the one place that
|
||||||
|
// actually has real entropy per browser session -- crypto.getRandomValues()
|
||||||
|
// -- as `simEntropy` on the Module config object (see meshcore-solo-site's
|
||||||
|
// bootInstance()/bootRepeater()), read back here the same way
|
||||||
|
// simInstanceTag already is. Defaults to 0 (this function's old, sole
|
||||||
|
// behavior) if a host page doesn't set it, so nothing else changes.
|
||||||
|
inline uint32_t sim_instance_entropy() {
|
||||||
|
return (uint32_t)EM_ASM_INT({
|
||||||
|
return (typeof Module !== 'undefined' && Module['simEntropy']) ? (Module['simEntropy'] >>> 0) : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
inline uint32_t sim_instance_salt() { return 0; }
|
inline uint32_t sim_instance_salt() { return 0; }
|
||||||
|
inline uint32_t sim_instance_entropy() { return 0; }
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+10
-2
@@ -10,12 +10,20 @@
|
|||||||
// in sim_instance_salt() so that two module instances of the SAME compiled
|
// in sim_instance_salt() so that two module instances of the SAME compiled
|
||||||
// binary (the browser's two companion_radio instances) can't end up with
|
// binary (the browser's two companion_radio instances) can't end up with
|
||||||
// the same seed -- see SimInstance.h for why that's a real risk here, not
|
// the same seed -- see SimInstance.h for why that's a real risk here, not
|
||||||
// a hypothetical one.
|
// a hypothetical one. sim_instance_entropy() (also SimInstance.h) mixes in
|
||||||
|
// real crypto.getRandomValues()-sourced entropy from the host page on top
|
||||||
|
// of that -- salt alone is a pure function of the tag string ('hero'/'B'/
|
||||||
|
// 'R'), so it's identical across two genuinely different browser tabs that
|
||||||
|
// both boot a 'hero' instance; combined with time(NULL)'s 1-second
|
||||||
|
// resolution and a WASM heap pointer that's fully deterministic (no ASLR
|
||||||
|
// inside the sandbox) across independent boots, two different real visitors
|
||||||
|
// landing on the same wall-clock second used to get byte-identical
|
||||||
|
// generated identities without this.
|
||||||
class SimRNG : public mesh::RNG {
|
class SimRNG : public mesh::RNG {
|
||||||
public:
|
public:
|
||||||
SimRNG() { }
|
SimRNG() { }
|
||||||
void begin() {
|
void begin() {
|
||||||
unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this ^ sim_instance_salt();
|
unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this ^ sim_instance_salt() ^ sim_instance_entropy();
|
||||||
srand(seed);
|
srand(seed);
|
||||||
}
|
}
|
||||||
void random(uint8_t* dest, size_t sz) override {
|
void random(uint8_t* dest, size_t sz) override {
|
||||||
|
|||||||
@@ -116,7 +116,14 @@ public:
|
|||||||
// time(NULL) second, same `rand()` process state, often the same
|
// time(NULL) second, same `rand()` process state, often the same
|
||||||
// `this` address across independent-but-identically-laid-out linear
|
// `this` address across independent-but-identically-laid-out linear
|
||||||
// memories) and end up with correlated "random" behaviour.
|
// memories) and end up with correlated "random" behaviour.
|
||||||
return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand() ^ sim_instance_salt();
|
// sim_instance_entropy(): same reasoning, but for two genuinely
|
||||||
|
// different browser tabs/machines both booting a same-tagged instance
|
||||||
|
// (e.g. two different visitors' 'hero') -- salt alone is a pure
|
||||||
|
// function of the tag string, identical for both; this mixes in real
|
||||||
|
// crypto.getRandomValues()-sourced entropy from the host page. See
|
||||||
|
// SimInstance.h and SimRNG.h for the full story (found while testing
|
||||||
|
// meshcore-solo-site's cross-visitor relay bridge).
|
||||||
|
return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand() ^ sim_instance_salt() ^ sim_instance_entropy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void getFreqBounds(float& min_mhz, float& max_mhz) const {
|
void getFreqBounds(float& min_mhz, float& max_mhz) const {
|
||||||
|
|||||||
@@ -213,6 +213,11 @@ done
|
|||||||
# included in the build" -- confirmed by hitting exactly that during Phase
|
# included in the build" -- confirmed by hitting exactly that during Phase
|
||||||
# 3. Purely additive: nothing Phase 2's web/index.html already does
|
# 3. Purely additive: nothing Phase 2's web/index.html already does
|
||||||
# (sim_enqueue_key() with a plain number, no buffer marshaling) is affected.
|
# (sim_enqueue_key() with a plain number, no buffer marshaling) is affected.
|
||||||
|
#
|
||||||
|
# HEAPF32,HEAP32 (relay-bridge addition): sim_radio_get_params() writes its
|
||||||
|
# freq/bw (float) and sf/cr (int) results into a JS-malloc'd scratch buffer
|
||||||
|
# via out-params -- reading them back needs the typed-array views, same
|
||||||
|
# reasoning as HEAPU8 above for sim_radio_poll_tx()'s byte buffer.
|
||||||
"$EMXX" \
|
"$EMXX" \
|
||||||
"${OBJS[@]}" \
|
"${OBJS[@]}" \
|
||||||
-lidbfs.js \
|
-lidbfs.js \
|
||||||
@@ -222,7 +227,7 @@ done
|
|||||||
-sEXPORT_NAME=MeshCoreSim \
|
-sEXPORT_NAME=MeshCoreSim \
|
||||||
-sENVIRONMENT=web \
|
-sENVIRONMENT=web \
|
||||||
-sEXIT_RUNTIME=0 \
|
-sEXIT_RUNTIME=0 \
|
||||||
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8 \
|
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8,HEAPF32,HEAP32 \
|
||||||
-sEXPORTED_FUNCTIONS=_main,_malloc,_free \
|
-sEXPORTED_FUNCTIONS=_main,_malloc,_free \
|
||||||
-o "$OUT_DIR/meshcore_sim.js"
|
-o "$OUT_DIR/meshcore_sim.js"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user