fix(mesh): dedup across transports + persistent radio-contact blocklist
Two mesh fixes bundled so the deploy lands them together: Doubled messages (radio + federation): dedup at store_message now runs a third cross-transport check keyed on (sender_seq, plaintext, 120s). The existing (sender_pubkey, sender_seq) match missed the common case where the same envelope arrives via LoRa radio (sender_pubkey looked up from the firmware key) and again via Tor federation (sender_pubkey = archipelago ed25519), because the two lookups disagree. The new cross-transport match closes that gap without loosening legacy paths. Stale contacts after clear-all: meshcore's on-device contact table is persistent and reads back into peers on the next refresh_contacts, so the previous "nuclear" clear wiped app state for a few seconds before the old rows reappeared. New persistent `radio_contact_blocklist` (mesh-ignored-radio-contacts.json) captures the pubkeys present at clear-time; `refresh_contacts` filters them on read and the filter survives restart. Federation-synthetic peers are excluded from the snapshot so the list rebuilds normally on the next gossip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cd8763f468
commit
7e4fed7967
@@ -34,6 +34,7 @@ use tokio::sync::watch;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
const MESH_CONFIG_FILE: &str = "mesh-config.json";
|
||||
const MESH_IGNORED_RADIO_FILE: &str = "mesh-ignored-radio-contacts.json";
|
||||
|
||||
/// Derive a stable synthetic `contact_id` for a federation peer from its
|
||||
/// archipelago ed25519 pubkey. Mesh LoRa contacts use meshcore firmware's
|
||||
@@ -178,6 +179,27 @@ pub async fn save_config(data_dir: &Path, config: &MeshConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_ignored_radio_contacts(data_dir: &Path) -> Vec<String> {
|
||||
let path = data_dir.join(MESH_IGNORED_RADIO_FILE);
|
||||
if !path.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
match fs::read_to_string(&path).await {
|
||||
Ok(s) => serde_json::from_str::<Vec<String>>(&s).unwrap_or_default(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_ignored_radio_contacts(data_dir: &Path, pubkeys: &[String]) -> Result<()> {
|
||||
fs::create_dir_all(data_dir).await.ok();
|
||||
let content = serde_json::to_string_pretty(pubkeys)
|
||||
.context("Failed to serialize ignored-radio list")?;
|
||||
fs::write(data_dir.join(MESH_IGNORED_RADIO_FILE), content)
|
||||
.await
|
||||
.context("Failed to write ignored-radio list")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detect serial devices that could be mesh radios.
|
||||
/// Checks both Meshcore (via probe) and legacy Meshtastic paths.
|
||||
pub async fn detect_devices() -> Vec<String> {
|
||||
@@ -264,6 +286,19 @@ impl MeshService {
|
||||
// radio — which never happens for nodes that only share Tor.
|
||||
seed_federation_peers_into_mesh(&state, data_dir).await;
|
||||
|
||||
// Load the radio-contact blocklist so previously-wiped firmware
|
||||
// contacts stay hidden after restart. Without this, meshcore's
|
||||
// persistent on-device contact table regenerates the rows on the
|
||||
// next refresh_contacts cycle and the user sees stale entries
|
||||
// they already cleared.
|
||||
{
|
||||
let ignored = load_ignored_radio_contacts(data_dir).await;
|
||||
let mut set = state.radio_contact_blocklist.write().await;
|
||||
for pk in ignored {
|
||||
set.insert(pk);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
config,
|
||||
|
||||
Reference in New Issue
Block a user