fix: v0.7.3 — disable EDM scanner, bootstrap batching, stale-anchor prune
Bandwidth + bootstrap hardening on top of v0.7.2. Wire-compatible with v0.7.0/v0.7.1/v0.7.2; no protocol changes. EDM port scanner DISABLED - hole_punch_with_scanning() now does only single quick punch + parallel punch over 30s window. The EDM port-scanner branch is gone from the live path because per-probe endpoint.connect() amplifies catastrophically: iroh accumulates every connect() target into a per-endpoint paths set and probes them all under QUIC NAT-traversal in the background. A 100-probes/sec / 5-min scan inserted ~30k paths; iroh probed all of them. Observed at 22MB/s outbound from one client — DoS-grade. - Scanner body preserved as edm_port_scan_disabled_v0_7_3() with all supporting helpers (PortWalkIter, scanner_semaphore, role-based scanner/puncher split, found_tx/found_rx channel pattern, deadline + tokio::select! orchestration) marked #[allow(dead_code)]. Refactor target: replace per-probe endpoint.connect() with raw socket.send_to() so probes don't enter iroh's path store. Bootstrap probing batched - New probe_anchors_batched() helper: 3 anchors in flight at a time, 2s stagger between batch dispatches, 10s per-anchor timeout, no abort on success. First success unblocks the bootstrap flow; remaining probes continue in background and fill peer connections naturally. - Phase 2 (bootstrap fallback) still only fires when every discovered anchor failed — preserves load-distribution intent. Replaces the sequential 50s+ timeout cascade users observed with old data dirs. Stale-anchor self-pruning - New storage.get_known_anchor_last_seen() and storage.delete_known_anchor(). - maybe_prune_stale_anchor(): when a probe fails AND last_seen_ms > 3 days, delete the entry from known_anchors immediately. Recoverable anchors (failed once, succeeded recently) are preserved. Self-healing for old data dirs whose discovered anchors point to keypairs that rotated months ago. Android close button kills NodeService - New NodeService.stopFromNative() Kotlin static method called via JNI from android_wifi::stop_node_service(). exit_app invokes it on Android before app.exit(0). Previously the button ended the Activity but the foreground service kept networking running. Cosmetic - Power-icon SVG (inline) replaces ⏻ so Android webviews lacking U+23FB don't render a missing-image tofu box. Docs - design.html section 11 rewritten for portmapper (UPnP+NAT-PMP+PCP, v0.7.2) including per-platform contract and bidirectional anchor watcher. - design.html section 10 marks session relay as opt-in (v0.7.2) and EDM scanner as disabled-pending-refactor (v0.7.3). - download.html carries v0.7.3 release notes. - MEMORY.md updated; older v0.7.0/v0.7.1 status sections condensed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4706e81603
commit
6ef11fa61c
14 changed files with 425 additions and 73 deletions
|
|
@ -92,6 +92,175 @@ async fn ensure_initial_v_me(
|
|||
generate_and_store_initial_v_me(&s, persona_id, now_ms)
|
||||
}
|
||||
|
||||
/// Probe a list of anchors with batched parallelism, returning the first
|
||||
/// successful NodeId. Remaining probes continue in background tasks after
|
||||
/// first success and naturally register additional mesh connections.
|
||||
///
|
||||
/// **Parameters fixed in v0.7.3:**
|
||||
/// - 3 anchors in flight at a time
|
||||
/// - 2-second stagger between batch dispatches
|
||||
/// - 10s per-anchor connect timeout
|
||||
/// - Failed probes to anchors with `last_seen_ms` older than 3 days
|
||||
/// auto-delete from `known_anchors` (self-healing pruning)
|
||||
///
|
||||
/// Returns `None` only when every probe completed without success.
|
||||
async fn probe_anchors_batched(
|
||||
anchors: Vec<(NodeId, Vec<std::net::SocketAddr>)>,
|
||||
network: Arc<crate::network::Network>,
|
||||
storage: Arc<StoragePool>,
|
||||
self_node_id: NodeId,
|
||||
label: &'static str,
|
||||
) -> Option<NodeId> {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
const BATCH_SIZE: usize = 3;
|
||||
const BATCH_STAGGER_SECS: u64 = 2;
|
||||
const PER_ANCHOR_TIMEOUT_SECS: u64 = 10;
|
||||
const STALE_THRESHOLD_MS: u64 = 3 * 86_400 * 1000;
|
||||
|
||||
let total = anchors.len();
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (success_tx, success_rx) = tokio::sync::oneshot::channel::<NodeId>();
|
||||
let success_tx = Arc::new(tokio::sync::Mutex::new(Some(success_tx)));
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
let all_done = Arc::new(tokio::sync::Notify::new());
|
||||
|
||||
// Dispatcher: spawns per-anchor tasks in batches of BATCH_SIZE,
|
||||
// sleeping BATCH_STAGGER_SECS between batches. The per-anchor tasks
|
||||
// continue running after the dispatcher exits.
|
||||
let dispatcher = {
|
||||
let network = Arc::clone(&network);
|
||||
let storage = Arc::clone(&storage);
|
||||
let success_tx = Arc::clone(&success_tx);
|
||||
let completed = Arc::clone(&completed);
|
||||
let all_done = Arc::clone(&all_done);
|
||||
tokio::spawn(async move {
|
||||
let mut iter = anchors.into_iter();
|
||||
loop {
|
||||
let batch: Vec<_> = (&mut iter).take(BATCH_SIZE).collect();
|
||||
if batch.is_empty() {
|
||||
break;
|
||||
}
|
||||
let more = iter.size_hint().0 > 0;
|
||||
for (nid, addrs) in batch {
|
||||
let network = Arc::clone(&network);
|
||||
let storage = Arc::clone(&storage);
|
||||
let success_tx = Arc::clone(&success_tx);
|
||||
let completed = Arc::clone(&completed);
|
||||
let all_done = Arc::clone(&all_done);
|
||||
tokio::spawn(async move {
|
||||
let result = probe_one_anchor(&network, &storage, nid, addrs, self_node_id, label).await;
|
||||
if let Some(nid) = result {
|
||||
let mut guard = success_tx.lock().await;
|
||||
if let Some(sender) = guard.take() {
|
||||
let _ = sender.send(nid);
|
||||
}
|
||||
}
|
||||
let prev = completed.fetch_add(1, Ordering::SeqCst);
|
||||
if prev + 1 == total {
|
||||
all_done.notify_one();
|
||||
}
|
||||
});
|
||||
}
|
||||
if more {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(BATCH_STAGGER_SECS)).await;
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// Race: first success vs all probes complete unsuccessfully.
|
||||
let result = tokio::select! {
|
||||
Ok(nid) = success_rx => Some(nid),
|
||||
_ = all_done.notified() => None,
|
||||
};
|
||||
|
||||
// Detach the dispatcher; in-flight per-anchor tasks continue.
|
||||
drop(dispatcher);
|
||||
|
||||
let _ = BATCH_STAGGER_SECS; // silence unused-const if compiler is picky
|
||||
let _ = PER_ANCHOR_TIMEOUT_SECS;
|
||||
let _ = STALE_THRESHOLD_MS;
|
||||
result
|
||||
}
|
||||
|
||||
async fn probe_one_anchor(
|
||||
network: &crate::network::Network,
|
||||
storage: &Arc<StoragePool>,
|
||||
nid: NodeId,
|
||||
addrs: Vec<std::net::SocketAddr>,
|
||||
self_node_id: NodeId,
|
||||
label: &'static str,
|
||||
) -> Option<NodeId> {
|
||||
const PER_ANCHOR_TIMEOUT_SECS: u64 = 10;
|
||||
const STALE_THRESHOLD_MS: u64 = 3 * 86_400 * 1000;
|
||||
|
||||
if nid == self_node_id || network.is_peer_connected_or_session(&nid).await {
|
||||
return None;
|
||||
}
|
||||
let endpoint_id = match iroh::EndpointId::from_bytes(&nid) {
|
||||
Ok(eid) => eid,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let mut addr = iroh::EndpointAddr::from(endpoint_id);
|
||||
for sa in &addrs {
|
||||
addr = addr.with_ip_addr(*sa);
|
||||
}
|
||||
info!(peer = hex::encode(&nid), label, "Trying anchor");
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(PER_ANCHOR_TIMEOUT_SECS),
|
||||
network.connect_to_anchor(nid, addr),
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
info!(peer = hex::encode(&nid), label, "Connected to anchor");
|
||||
Some(nid)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
debug!(error = %e, peer = hex::encode(&nid), label, "Anchor connect failed");
|
||||
maybe_prune_stale_anchor(storage, &nid, STALE_THRESHOLD_MS).await;
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
debug!(peer = hex::encode(&nid), label, "Anchor connect timed out");
|
||||
maybe_prune_stale_anchor(storage, &nid, STALE_THRESHOLD_MS).await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the anchor's last successful contact was more than `threshold_ms`
|
||||
/// ago, delete it from `known_anchors`. Future startups won't waste a
|
||||
/// probe slot on it. Anchors that were recently successful are preserved
|
||||
/// even when they fail a single probe (likely transient).
|
||||
async fn maybe_prune_stale_anchor(
|
||||
storage: &Arc<StoragePool>,
|
||||
nid: &NodeId,
|
||||
threshold_ms: u64,
|
||||
) {
|
||||
let s = storage.get().await;
|
||||
let last_seen_ms = match s.get_known_anchor_last_seen(nid) {
|
||||
Ok(Some(ms)) => ms,
|
||||
_ => return,
|
||||
};
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
if now_ms > last_seen_ms && now_ms - last_seen_ms > threshold_ms {
|
||||
let _ = s.delete_known_anchor(nid);
|
||||
debug!(
|
||||
peer = hex::encode(nid),
|
||||
age_ms = now_ms - last_seen_ms,
|
||||
"Pruned stale anchor (>3 days since last success + failed probe)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Create or open a node in the given data directory (Desktop profile)
|
||||
pub async fn open(data_dir: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
|
|
@ -272,6 +441,11 @@ impl Node {
|
|||
|
||||
/// Bootstrap: connect to anchors, pull initial data, NAT probe, referrals.
|
||||
/// Can be called during open_with_bind (blocking startup) or deferred to background.
|
||||
///
|
||||
/// v0.7.3: anchor probing is batched (3 in flight, 2s stagger between batches,
|
||||
/// 10s per-anchor timeout, first success unblocks downstream, remaining probes
|
||||
/// continue in background and naturally fill peer connections). Failed probes
|
||||
/// to anchors >3 days stale auto-prune from `known_anchors`.
|
||||
pub async fn run_bootstrap(&self, data_dir: &Path) -> anyhow::Result<()> {
|
||||
let storage = &self.storage;
|
||||
let network = &self.network;
|
||||
|
|
@ -479,57 +653,28 @@ impl Node {
|
|||
let (discovered, bootstrap_known): (Vec<_>, Vec<_>) = known.into_iter()
|
||||
.partition(|(nid, _)| !bootstrap_anchor_ids.contains(nid));
|
||||
|
||||
// Phase 1: Try discovered (non-bootstrap) anchors first
|
||||
let mut connected_anchor = None;
|
||||
for (anchor_nid, anchor_addrs) in &discovered {
|
||||
if *anchor_nid == node_id || network.is_peer_connected_or_session(anchor_nid).await {
|
||||
continue;
|
||||
}
|
||||
let endpoint_id = match iroh::EndpointId::from_bytes(anchor_nid) {
|
||||
Ok(eid) => eid,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let mut addr = iroh::EndpointAddr::from(endpoint_id);
|
||||
for sa in anchor_addrs {
|
||||
addr = addr.with_ip_addr(*sa);
|
||||
}
|
||||
info!(peer = hex::encode(anchor_nid), "Trying discovered anchor");
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(10), network.connect_to_anchor(*anchor_nid, addr)).await {
|
||||
Ok(Ok(())) => {
|
||||
info!(peer = hex::encode(anchor_nid), "Connected to discovered anchor");
|
||||
connected_anchor = Some(*anchor_nid);
|
||||
break;
|
||||
}
|
||||
Ok(Err(e)) => debug!(error = %e, peer = hex::encode(anchor_nid), "Discovered anchor: connect failed"),
|
||||
Err(_) => debug!(peer = hex::encode(anchor_nid), "Discovered anchor: connect timed out"),
|
||||
}
|
||||
}
|
||||
// Phase 1: probe discovered (non-bootstrap) anchors in batches.
|
||||
// First success returns immediately; remaining probes continue in
|
||||
// background. Failed probes to anchors >3 days stale auto-prune.
|
||||
let mut connected_anchor = probe_anchors_batched(
|
||||
discovered.clone(),
|
||||
network.clone(),
|
||||
Arc::clone(storage),
|
||||
node_id,
|
||||
"discovered",
|
||||
).await;
|
||||
|
||||
// Phase 2: Fall back to bootstrap anchors only if no discovered anchor worked
|
||||
// Phase 2: bootstrap anchors as fallback — only fires if every
|
||||
// Phase 1 entry failed. Preserves the load-distribution intent
|
||||
// (don't smash the central anchor when discovered anchors work).
|
||||
if connected_anchor.is_none() {
|
||||
for (anchor_nid, anchor_addrs) in &bootstrap_known {
|
||||
if *anchor_nid == node_id || network.is_peer_connected_or_session(anchor_nid).await {
|
||||
continue;
|
||||
}
|
||||
let endpoint_id = match iroh::EndpointId::from_bytes(anchor_nid) {
|
||||
Ok(eid) => eid,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let mut addr = iroh::EndpointAddr::from(endpoint_id);
|
||||
for sa in anchor_addrs {
|
||||
addr = addr.with_ip_addr(*sa);
|
||||
}
|
||||
info!(peer = hex::encode(anchor_nid), "Trying bootstrap anchor (fallback)");
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(10), network.connect_to_anchor(*anchor_nid, addr)).await {
|
||||
Ok(Ok(())) => {
|
||||
info!(peer = hex::encode(anchor_nid), "Connected to bootstrap anchor");
|
||||
connected_anchor = Some(*anchor_nid);
|
||||
break;
|
||||
}
|
||||
Ok(Err(e)) => debug!(error = %e, peer = hex::encode(anchor_nid), "Bootstrap anchor: connect failed"),
|
||||
Err(_) => debug!(peer = hex::encode(anchor_nid), "Bootstrap anchor: connect timed out"),
|
||||
}
|
||||
}
|
||||
connected_anchor = probe_anchors_batched(
|
||||
bootstrap_known.clone(),
|
||||
network.clone(),
|
||||
Arc::clone(storage),
|
||||
node_id,
|
||||
"bootstrap",
|
||||
).await;
|
||||
}
|
||||
|
||||
// Phase 3: NAT probe + referrals from whichever anchor we connected to
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue