diff --git a/src/node/handlers/discovery.rs b/src/node/handlers/discovery.rs index e3d8c7b..fff28a6 100644 --- a/src/node/handlers/discovery.rs +++ b/src/node/handlers/discovery.rs @@ -439,14 +439,30 @@ impl Node { let origin_coords = self.tree_state().my_coords().clone(); let request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0); - // Send only to tree peers whose bloom filter contains the target - let peer_addrs: Vec = self + // Prefer tree peers whose bloom filter contains the target. + let mut peer_addrs: Vec = self .peers .iter() .filter(|(addr, peer)| self.is_tree_peer(addr) && peer.may_reach(target)) .map(|(addr, _)| *addr) .collect(); + // Edge fallback: if no tree peer advertises the target — always the case + // for a *directly connected* target, since a neighbour is never in another + // peer's bloom — flood the request to EVERY peer we can send to, not just + // tree peers. Crucially this includes the target itself when it's a direct + // (non-tree) neighbour: a node answers a lookup for its own address, so the + // querier learns its coordinates and can route to it. Without this a + // mesh-edge node can't reach its own neighbours. + if peer_addrs.is_empty() { + peer_addrs = self + .peers + .iter() + .filter(|(_, peer)| peer.can_send()) + .map(|(addr, _)| *addr) + .collect(); + } + let peer_count = peer_addrs.len(); debug!( @@ -509,14 +525,22 @@ impl Node { return; } - // Bloom filter pre-check: if no peer's filter contains the target, - // it's not in the mesh — skip the lookup and record as failure. + // Bloom filter pre-check: normally, if no peer's filter contains the + // target we skip. But a *directly connected* peer is never in any other + // peer's bloom (you reach it directly, not through them), so a mesh-edge + // node — one whose only peers are direct links (BLE / Wi-Fi Aware / LAN), + // with sparse blooms — could never discover coordinates for its own + // neighbours and would fail to route to them. Only suppress on a bloom + // miss when we have several peers whose blooms we can actually trust; + // otherwise fall through and flood the lookup (bounded by TTL + the + // `sent == 0` guard below). let reachable = self.peers.values().any(|peer| peer.may_reach(dest)); - if !reachable { + if !reachable && self.peers.len() > 2 { self.metrics().discovery.req_bloom_miss.inc(); self.discovery_backoff.record_failure(dest); debug!( target_node = %self.peer_display_name(dest), + peers = self.peers.len(), "Discovery skipped, target not in any peer bloom filter" ); return; diff --git a/src/node/handlers/session.rs b/src/node/handlers/session.rs index 3bd6230..5a1d788 100644 --- a/src/node/handlers/session.rs +++ b/src/node/handlers/session.rs @@ -2140,6 +2140,15 @@ impl Node { } if let Err(e) = self.send_ipv6_packet(&dest_addr, &ipv6_packet).await { debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to send TUN packet via session"); + // An established session can still have no route when its + // coordinates were never learned or went stale — e.g. a + // platform-pushed peer (Wi-Fi Aware / LAN) whose Noise session + // came up before tree discovery. Unlike session initiation + // (below), this path didn't trigger a lookup, so the route + // never warmed and every packet was silently dropped. Kick + // discovery (rate-limited internally); the app's retransmit + // then succeeds once coordinates arrive. + self.maybe_initiate_lookup(&dest_addr).await; } return; } diff --git a/src/node/mod.rs b/src/node/mod.rs index 871fe33..04ec487 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -2910,6 +2910,26 @@ impl Node { (outbound_tx, tun_rx) } + /// Set up an **app-owned DNS resolver**: an embedder that answers `.fips` + /// queries itself (e.g. the Android VpnService packet pump, which has no + /// system DNS socket) returns each resolved identity through the sender this + /// returns. The `run_rx_loop` consumes them and calls + /// [`Self::register_identity`] — the same identity-cache population (and hence + /// route warming) the built-in [`crate::upper::dns::run_dns_responder`] does. + /// + /// Without this the embedder's answers resolve the AAAA but leave the node's + /// identity cache empty, so the first outbound packet to a freshly-resolved + /// `.fips` has no cached pubkey to open a session with and is dropped. + /// + /// Call after [`Node::new`] and **before** [`Self::start`], like + /// [`Self::enable_app_owned_tun`]. + pub fn enable_app_owned_dns(&mut self) -> crate::upper::dns::DnsIdentityTx { + let size = self.config().node.buffers.tun_channel.max(1); + let (identity_tx, identity_rx) = tokio::sync::mpsc::channel(size); + self.dns_identity_rx = Some(identity_rx); + identity_tx + } + // === Sending === /// Encrypt and send a link-layer message to an authenticated peer.