mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 00:26:59 +00:00
Add FMP node profile negotiation with mixed-profile integration tests
NodeProfile enum (Full/NonRouting/Leaf) with FMP feature bitfield: bits 0-2 profile, bits 3-6 MMP wants/provides, bit 7 bloom filter size negotiable. Bloom size TLV always sent with min=max=1KB. Config mapping: leaf_only -> Leaf, disable_routing -> NonRouting. Profile and agreed bloom size stored on PeerConnection and ActivePeer. Handshake msg2/msg3 carry FMP negotiation payload with profile validation and bloom size agreement. MMP report sending gated by profile wants/provides. Parent selection and routing constraints skip non-full peers. One-way bloom filters for non-routing peers (F inserts N as dependent). Leaf mode: single-peer enforcement, suppress tree announces and discovery forwarding. Mixed-profile integration test: A(Full) + B(Full) + C(NonRouting) + D(Leaf) with 9 connectivity assertions.
This commit is contained in:
+12
-2
@@ -12,14 +12,17 @@ use std::collections::HashMap;
|
||||
use tracing::debug;
|
||||
|
||||
impl Node {
|
||||
/// Collect inbound filters from all peers for outgoing filter computation.
|
||||
/// Collect inbound filters from full tree peers for outgoing filter computation.
|
||||
///
|
||||
/// Returns a map of (peer_node_addr -> filter) for peers that
|
||||
/// have sent us a FilterAnnounce.
|
||||
/// have sent us a FilterAnnounce. Non-routing and leaf peers are
|
||||
/// excluded (they don't send filters; their identity is covered
|
||||
/// via leaf_dependents).
|
||||
fn peer_inbound_filters(&self) -> HashMap<NodeAddr, BloomFilter> {
|
||||
let mut filters = HashMap::new();
|
||||
for (addr, peer) in &self.peers {
|
||||
if self.is_tree_peer(addr)
|
||||
&& peer.peer_profile() == crate::protocol::NodeProfile::Full
|
||||
&& let Some(filter) = peer.inbound_filter()
|
||||
{
|
||||
filters.insert(*addr, filter.clone());
|
||||
@@ -98,7 +101,14 @@ impl Node {
|
||||
}
|
||||
|
||||
/// Send pending rate-limited filter announces whose debounce has expired.
|
||||
///
|
||||
/// Non-routing nodes do not send filters (they receive only).
|
||||
pub(super) async fn send_pending_filter_announces(&mut self) {
|
||||
// Non-routing and leaf nodes don't send bloom filters
|
||||
if self.node_profile != crate::protocol::NodeProfile::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
|
||||
@@ -20,7 +20,11 @@ impl Node {
|
||||
/// 4. Lazy purge expired entries
|
||||
/// 5. If we're the target, generate and send response
|
||||
/// 6. If TTL > 0, forward to tree peers whose bloom filter matches
|
||||
pub(in crate::node) async fn handle_lookup_request(&mut self, from: &NodeAddr, payload: &[u8]) {
|
||||
pub(in crate::node) async fn handle_lookup_request(
|
||||
&mut self,
|
||||
from: &NodeAddr,
|
||||
payload: &[u8],
|
||||
) {
|
||||
self.stats_mut().discovery.req_received += 1;
|
||||
|
||||
let request = match LookupRequest::decode(payload) {
|
||||
@@ -48,8 +52,10 @@ impl Node {
|
||||
}
|
||||
|
||||
// Record for reverse-path forwarding and dedup
|
||||
self.recent_requests
|
||||
.insert(request.request_id, RecentRequest::new(*from, now_ms));
|
||||
self.recent_requests.insert(
|
||||
request.request_id,
|
||||
RecentRequest::new(*from, now_ms),
|
||||
);
|
||||
|
||||
// Lazy purge expired entries
|
||||
self.purge_expired_requests(now_ms);
|
||||
@@ -70,10 +76,7 @@ impl Node {
|
||||
if request.can_forward() {
|
||||
// Transit-side rate limit: collapse rapid-fire lookups for the
|
||||
// same target from misbehaving nodes generating fresh request_ids.
|
||||
if !self
|
||||
.discovery_forward_limiter
|
||||
.should_forward(&request.target)
|
||||
{
|
||||
if !self.discovery_forward_limiter.should_forward(&request.target) {
|
||||
self.stats_mut().discovery.req_forward_rate_limited += 1;
|
||||
debug!(
|
||||
request_id = request.request_id,
|
||||
@@ -189,8 +192,11 @@ impl Node {
|
||||
// Verify the proof signature
|
||||
let (xonly, _parity) = target_pubkey.x_only_public_key();
|
||||
let peer_id = PeerIdentity::from_pubkey(xonly);
|
||||
let proof_data =
|
||||
LookupResponse::proof_bytes(response.request_id, &target, &response.target_coords);
|
||||
let proof_data = LookupResponse::proof_bytes(
|
||||
response.request_id,
|
||||
&target,
|
||||
&response.target_coords,
|
||||
);
|
||||
if !peer_id.verify(&proof_data, &response.proof) {
|
||||
self.stats_mut().discovery.resp_proof_failed += 1;
|
||||
warn!(
|
||||
@@ -214,8 +220,12 @@ impl Node {
|
||||
"Discovery succeeded, proof verified, route cached"
|
||||
);
|
||||
|
||||
self.coord_cache
|
||||
.insert_with_path_mtu(target, response.target_coords, now_ms, path_mtu);
|
||||
self.coord_cache.insert_with_path_mtu(
|
||||
target,
|
||||
response.target_coords,
|
||||
now_ms,
|
||||
path_mtu,
|
||||
);
|
||||
|
||||
// Clean up pending lookup tracking
|
||||
self.pending_lookups.remove(&target);
|
||||
@@ -252,11 +262,15 @@ impl Node {
|
||||
let our_coords = self.tree_state().my_coords().clone();
|
||||
|
||||
// Sign proof: Identity::sign hashes with SHA-256 internally
|
||||
let proof_data =
|
||||
LookupResponse::proof_bytes(request.request_id, &request.target, &our_coords);
|
||||
let proof_data = LookupResponse::proof_bytes(request.request_id, &request.target, &our_coords);
|
||||
let proof = self.identity().sign(&proof_data);
|
||||
|
||||
let response = LookupResponse::new(request.request_id, request.target, our_coords, proof);
|
||||
let response = LookupResponse::new(
|
||||
request.request_id,
|
||||
request.target,
|
||||
our_coords,
|
||||
proof,
|
||||
);
|
||||
|
||||
// Route toward origin via reverse path.
|
||||
let next_hop_addr = if let Some(recent) = self.recent_requests.get(&request.request_id) {
|
||||
@@ -283,10 +297,7 @@ impl Node {
|
||||
);
|
||||
|
||||
let encoded = response.encode();
|
||||
if let Err(e) = self
|
||||
.send_encrypted_link_message(&next_hop_addr, &encoded)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = self.send_encrypted_link_message(&next_hop_addr, &encoded).await {
|
||||
debug!(
|
||||
next_hop = %self.peer_display_name(&next_hop_addr),
|
||||
error = %e,
|
||||
@@ -309,20 +320,33 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect tree peers whose bloom filter contains the target
|
||||
// Leaf nodes don't forward discovery requests
|
||||
if self.node_profile == crate::protocol::NodeProfile::Leaf {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect full tree peers whose bloom filter contains the target
|
||||
let forward_to: Vec<NodeAddr> = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(addr, peer)| self.is_tree_peer(addr) && peer.may_reach(&request.target))
|
||||
.filter(|(addr, peer)| {
|
||||
peer.peer_profile() == crate::protocol::NodeProfile::Full
|
||||
&& self.is_tree_peer(addr)
|
||||
&& peer.may_reach(&request.target)
|
||||
})
|
||||
.map(|(addr, _)| *addr)
|
||||
.collect();
|
||||
|
||||
// Fallback: if no tree peer matches, try non-tree bloom-matching peers
|
||||
// Fallback: if no tree peer matches, try non-tree full bloom-matching peers
|
||||
let (forward_to, used_fallback) = if forward_to.is_empty() {
|
||||
let fallback: Vec<NodeAddr> = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(addr, peer)| !self.is_tree_peer(addr) && peer.may_reach(&request.target))
|
||||
.filter(|(addr, peer)| {
|
||||
peer.peer_profile() == crate::protocol::NodeProfile::Full
|
||||
&& !self.is_tree_peer(addr)
|
||||
&& peer.may_reach(&request.target)
|
||||
})
|
||||
.map(|(addr, _)| *addr)
|
||||
.collect();
|
||||
if fallback.is_empty() {
|
||||
@@ -383,11 +407,15 @@ 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
|
||||
// Send only to full tree peers whose bloom filter contains the target
|
||||
let peer_addrs: Vec<NodeAddr> = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(addr, peer)| self.is_tree_peer(addr) && peer.may_reach(target))
|
||||
.filter(|(addr, peer)| {
|
||||
peer.peer_profile() == crate::protocol::NodeProfile::Full
|
||||
&& self.is_tree_peer(addr)
|
||||
&& peer.may_reach(target)
|
||||
})
|
||||
.map(|(addr, _)| *addr)
|
||||
.collect();
|
||||
|
||||
@@ -471,8 +499,7 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending_lookups
|
||||
.insert(*dest, PendingLookup::new(now_ms));
|
||||
self.pending_lookups.insert(*dest, PendingLookup::new(now_ms));
|
||||
let ttl = self.config.node.discovery.ttl;
|
||||
let sent = self.initiate_lookup(dest, ttl).await;
|
||||
|
||||
|
||||
@@ -176,7 +176,8 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
// Bloom filter cleanup: clear state for removed peer, mark all remaining peers
|
||||
// Bloom filter cleanup: remove dependent (non-routing/leaf peers), clear state
|
||||
self.bloom_state.remove_leaf_dependent(node_addr);
|
||||
self.bloom_state.remove_peer_state(node_addr);
|
||||
let remaining_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(remaining_peers);
|
||||
|
||||
@@ -124,8 +124,8 @@ impl Node {
|
||||
packet.timestamp_ms,
|
||||
);
|
||||
|
||||
// Create negotiation payload for msg2
|
||||
let neg_payload = NegotiationPayload::new(1, 1, 0).encode();
|
||||
// Create FMP negotiation payload for msg2 (includes profile, MMP bits, bloom TLV)
|
||||
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile).encode();
|
||||
|
||||
let our_keypair = self.identity.keypair();
|
||||
let noise_msg1 = &packet.data[header.noise_msg1_offset..];
|
||||
@@ -383,12 +383,12 @@ impl Node {
|
||||
|
||||
let conn = self.connections.get_mut(&link_id).unwrap();
|
||||
|
||||
// Create negotiation payload for msg3
|
||||
let neg_payload = NegotiationPayload::new(1, 1, 0).encode();
|
||||
// Create FMP negotiation payload for msg3 (includes profile, MMP bits, bloom TLV)
|
||||
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile).encode();
|
||||
|
||||
// Process Noise msg2 and generate msg3
|
||||
let noise_msg2 = &packet.data[header.noise_msg2_offset..];
|
||||
let (msg3_bytes, _received_negotiation) = match conn.complete_handshake(noise_msg2, Some(&neg_payload), packet.timestamp_ms) {
|
||||
let (msg3_bytes, received_negotiation) = match conn.complete_handshake(noise_msg2, Some(&neg_payload), packet.timestamp_ms) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -401,6 +401,18 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Process peer's FMP negotiation payload from msg2
|
||||
if let Some(neg_bytes) = &received_negotiation {
|
||||
match process_fmp_negotiation(self.node_profile, conn, neg_bytes) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
warn!(link_id = %link_id, error = %e, "FMP negotiation failed");
|
||||
conn.mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store their index
|
||||
conn.set_their_index(header.sender_idx);
|
||||
conn.set_source_addr(packet.remote_addr.clone());
|
||||
@@ -701,7 +713,7 @@ impl Node {
|
||||
|
||||
// Process msg3 — learns initiator's identity and epoch
|
||||
let noise_msg3 = &packet.data[header.noise_msg3_offset..];
|
||||
let _received_negotiation = match conn.complete_handshake_msg3(noise_msg3, packet.timestamp_ms) {
|
||||
let received_negotiation = match conn.complete_handshake_msg3(noise_msg3, packet.timestamp_ms) {
|
||||
Ok(neg) => neg,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -719,6 +731,19 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Process peer's FMP negotiation payload from msg3
|
||||
if let Some(neg_bytes) = &received_negotiation {
|
||||
match process_fmp_negotiation(self.node_profile, conn, neg_bytes) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
warn!(link_id = %link_id, error = %e, "FMP negotiation failed");
|
||||
self.connections.remove(&link_id);
|
||||
self.remove_link(&link_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Learn peer identity from msg3
|
||||
let peer_identity = match conn.expected_identity() {
|
||||
Some(id) => *id,
|
||||
@@ -1042,12 +1067,34 @@ impl Node {
|
||||
/// Promote a connection to active peer after successful authentication.
|
||||
///
|
||||
/// Handles cross-connection detection and resolution using tie-breaker rules.
|
||||
/// Leaf nodes enforce single-peer constraint.
|
||||
pub(in crate::node) fn promote_connection(
|
||||
&mut self,
|
||||
link_id: LinkId,
|
||||
verified_identity: PeerIdentity,
|
||||
current_time_ms: u64,
|
||||
) -> Result<PromotionResult, NodeError> {
|
||||
// Leaf nodes: reject if we already have a peer (single-peer enforcement)
|
||||
let peer_node_addr_check = *verified_identity.node_addr();
|
||||
if self.node_profile == crate::protocol::NodeProfile::Leaf
|
||||
&& !self.peers.is_empty()
|
||||
&& !self.peers.contains_key(&peer_node_addr_check)
|
||||
{
|
||||
info!(
|
||||
peer = %self.peer_display_name(&peer_node_addr_check),
|
||||
link_id = %link_id,
|
||||
"Leaf node rejecting additional peer (single-peer enforcement)"
|
||||
);
|
||||
// Clean up the connection
|
||||
if let Some(conn) = self.connections.remove(&link_id)
|
||||
&& let Some(idx) = conn.our_index()
|
||||
{
|
||||
let _ = self.index_allocator.free(idx);
|
||||
}
|
||||
self.remove_link(&link_id);
|
||||
return Err(NodeError::MaxPeersExceeded { max: 1 });
|
||||
}
|
||||
|
||||
// Remove the connection from pending
|
||||
let mut connection = self
|
||||
.connections
|
||||
@@ -1089,6 +1136,10 @@ impl Node {
|
||||
})?.clone();
|
||||
let link_stats = connection.link_stats().clone();
|
||||
let remote_epoch = connection.remote_epoch();
|
||||
let peer_profile = connection.peer_profile()
|
||||
.unwrap_or(crate::protocol::NodeProfile::Full);
|
||||
let agreed_bloom_size_class = connection.agreed_bloom_size_class()
|
||||
.unwrap_or(crate::bloom::V1_SIZE_CLASS);
|
||||
|
||||
let peer_node_addr = *verified_identity.node_addr();
|
||||
let is_outbound = connection.is_outbound();
|
||||
@@ -1131,6 +1182,9 @@ impl Node {
|
||||
is_outbound,
|
||||
&self.config.node.mmp,
|
||||
remote_epoch,
|
||||
self.node_profile,
|
||||
peer_profile,
|
||||
agreed_bloom_size_class,
|
||||
);
|
||||
new_peer.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
|
||||
|
||||
@@ -1140,6 +1194,12 @@ impl Node {
|
||||
self.retry_pending.remove(&peer_node_addr);
|
||||
self.register_identity(peer_node_addr, verified_identity.pubkey_full());
|
||||
|
||||
// Non-routing peers don't send filters; include them as
|
||||
// dependents so our bloom filter advertises their identity.
|
||||
if peer_profile != crate::protocol::NodeProfile::Full {
|
||||
self.bloom_state.add_leaf_dependent(peer_node_addr);
|
||||
}
|
||||
|
||||
debug!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
winner_link = %link_id,
|
||||
@@ -1220,6 +1280,9 @@ impl Node {
|
||||
is_outbound,
|
||||
&self.config.node.mmp,
|
||||
remote_epoch,
|
||||
self.node_profile,
|
||||
peer_profile,
|
||||
agreed_bloom_size_class,
|
||||
);
|
||||
new_peer.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
|
||||
if let Some(ts) = old_announce_ts {
|
||||
@@ -1232,6 +1295,12 @@ impl Node {
|
||||
self.retry_pending.remove(&peer_node_addr);
|
||||
self.register_identity(peer_node_addr, verified_identity.pubkey_full());
|
||||
|
||||
// Non-routing peers don't send filters; include them as
|
||||
// dependents so our bloom filter advertises their identity.
|
||||
if peer_profile != crate::protocol::NodeProfile::Full {
|
||||
self.bloom_state.add_leaf_dependent(peer_node_addr);
|
||||
}
|
||||
|
||||
info!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
link_id = %link_id,
|
||||
@@ -1243,4 +1312,37 @@ impl Node {
|
||||
Ok(PromotionResult::Promoted(peer_node_addr))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Process an FMP negotiation payload received from a peer.
|
||||
///
|
||||
/// Decodes the payload, validates profile pairing, agrees on bloom
|
||||
/// filter size, and stores the results on the PeerConnection.
|
||||
fn process_fmp_negotiation(
|
||||
our_profile: crate::protocol::NodeProfile,
|
||||
conn: &mut PeerConnection,
|
||||
neg_bytes: &[u8],
|
||||
) -> Result<(), crate::protocol::ProtocolError> {
|
||||
let our_payload = NegotiationPayload::fmp(1, 1, our_profile);
|
||||
let their_payload = NegotiationPayload::decode(neg_bytes)?;
|
||||
|
||||
// Validate profile pairing (at least one Full)
|
||||
let their_profile = their_payload.node_profile()?;
|
||||
NegotiationPayload::validate_profiles(our_profile, their_profile)?;
|
||||
|
||||
// Agree on bloom filter size
|
||||
let agreed_bloom = our_payload.agree_bloom_size(&their_payload)?;
|
||||
|
||||
conn.set_negotiation_results(their_profile, agreed_bloom);
|
||||
|
||||
debug!(
|
||||
link_id = %conn.link_id(),
|
||||
our_profile = ?our_profile,
|
||||
peer_profile = ?their_profile,
|
||||
agreed_bloom_size_class = agreed_bloom,
|
||||
"FMP negotiation complete"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -141,7 +141,8 @@ impl Node {
|
||||
.filter(|(_, p)| p.has_srtt())
|
||||
.map(|(a, p)| (*a, p.link_cost()))
|
||||
.collect();
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
||||
let skip = self.non_full_peers();
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs, &skip) {
|
||||
let new_seq = self.tree_state.my_declaration().sequence() + 1;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -194,22 +195,27 @@ impl Node {
|
||||
.cloned()
|
||||
.unwrap_or_else(|| peer.identity().short_npub());
|
||||
|
||||
let send_sr = peer.send_sr();
|
||||
let send_rr = peer.send_rr();
|
||||
|
||||
let Some(mmp) = peer.mmp_mut() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mode = mmp.mode();
|
||||
|
||||
// Sender reports: Full mode only
|
||||
// Sender reports: gated by mode, profile wants/provides, and timing
|
||||
if mode == MmpMode::Full
|
||||
&& send_sr
|
||||
&& mmp.sender.should_send_report(now)
|
||||
&& let Some(sr) = mmp.sender.build_report(now)
|
||||
{
|
||||
sender_reports.push((*node_addr, sr.encode()));
|
||||
}
|
||||
|
||||
// Receiver reports: Full and Lightweight modes
|
||||
// Receiver reports: gated by mode, profile wants/provides, and timing
|
||||
if mode != MmpMode::Minimal
|
||||
&& send_rr
|
||||
&& mmp.receiver.should_send_report(now)
|
||||
&& let Some(rr) = mmp.receiver.build_report(now)
|
||||
{
|
||||
|
||||
+128
-122
@@ -5,43 +5,42 @@
|
||||
//! Bloom filters, coordinate caches, transports, links, and peers.
|
||||
|
||||
mod bloom;
|
||||
mod discovery_rate_limit;
|
||||
mod handlers;
|
||||
mod lifecycle;
|
||||
mod rate_limit;
|
||||
mod retry;
|
||||
mod discovery_rate_limit;
|
||||
mod rate_limit;
|
||||
mod routing_error_rate_limit;
|
||||
pub(crate) mod session;
|
||||
pub(crate) mod session_wire;
|
||||
pub(crate) mod wire;
|
||||
pub(crate) mod stats;
|
||||
mod tree;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tree;
|
||||
pub(crate) mod wire;
|
||||
|
||||
use crate::bloom::BloomState;
|
||||
use crate::protocol::NodeProfile;
|
||||
use crate::cache::CoordCache;
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use crate::node::session::SessionEntry;
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use self::discovery_rate_limit::{DiscoveryBackoff, DiscoveryForwardRateLimiter};
|
||||
use self::rate_limit::HandshakeRateLimiter;
|
||||
use self::routing_error_rate_limit::RoutingErrorRateLimiter;
|
||||
use self::wire::{
|
||||
FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, build_encrypted, build_established_header,
|
||||
prepend_inner_header,
|
||||
};
|
||||
use crate::bloom::BloomState;
|
||||
use crate::cache::CoordCache;
|
||||
use crate::node::session::SessionEntry;
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use crate::transport::ethernet::EthernetTransport;
|
||||
use crate::transport::tcp::TcpTransport;
|
||||
use crate::transport::tor::TorTransport;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::transport::{
|
||||
Link, LinkId, PacketRx, PacketTx, TransportAddr, TransportError, TransportHandle, TransportId,
|
||||
};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::transport::tcp::TcpTransport;
|
||||
use crate::transport::tor::TorTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::transport::ethernet::EthernetTransport;
|
||||
use crate::tree::TreeState;
|
||||
use crate::upper::hosts::HostMap;
|
||||
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
|
||||
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use self::wire::{build_encrypted, build_established_header, prepend_inner_header, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP};
|
||||
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity};
|
||||
use rand::Rng;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
@@ -108,11 +107,7 @@ pub enum NodeError {
|
||||
SendFailed { node_addr: NodeAddr, reason: String },
|
||||
|
||||
#[error("mtu exceeded forwarding to {node_addr}: packet {packet_size} > mtu {mtu}")]
|
||||
MtuExceeded {
|
||||
node_addr: NodeAddr,
|
||||
packet_size: usize,
|
||||
mtu: u16,
|
||||
},
|
||||
MtuExceeded { node_addr: NodeAddr, packet_size: usize, mtu: u16 },
|
||||
|
||||
#[error("config error: {0}")]
|
||||
Config(#[from] ConfigError),
|
||||
@@ -278,6 +273,9 @@ pub struct Node {
|
||||
/// Whether this is a leaf-only node.
|
||||
is_leaf_only: bool,
|
||||
|
||||
/// Node profile derived from config (Full, NonRouting, Leaf).
|
||||
node_profile: NodeProfile,
|
||||
|
||||
// === Spanning Tree ===
|
||||
/// Local spanning tree state.
|
||||
tree_state: TreeState,
|
||||
@@ -370,10 +368,6 @@ pub struct Node {
|
||||
tun_reader_handle: Option<JoinHandle<()>>,
|
||||
/// TUN writer thread handle.
|
||||
tun_writer_handle: Option<JoinHandle<()>>,
|
||||
/// Shutdown pipe: writing to this fd unblocks the TUN reader thread on macOS.
|
||||
/// On Linux, deleting the interface via netlink serves the same purpose.
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: Option<std::os::unix::io::RawFd>,
|
||||
|
||||
// === DNS Responder ===
|
||||
/// Receiver for resolved identities from the DNS responder.
|
||||
@@ -454,6 +448,7 @@ impl Node {
|
||||
let identity = config.create_identity()?;
|
||||
let node_addr = *identity.node_addr();
|
||||
let is_leaf_only = config.is_leaf_only();
|
||||
let node_profile = config.node_profile();
|
||||
|
||||
let mut startup_epoch = [0u8; 8];
|
||||
rand::rng().fill_bytes(&mut startup_epoch);
|
||||
@@ -516,6 +511,7 @@ impl Node {
|
||||
config,
|
||||
state: NodeState::Created,
|
||||
is_leaf_only,
|
||||
node_profile,
|
||||
tree_state,
|
||||
bloom_state,
|
||||
coord_cache,
|
||||
@@ -544,8 +540,6 @@ impl Node {
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
@@ -558,7 +552,10 @@ impl Node {
|
||||
coords_response_rate_limiter: RoutingErrorRateLimiter::with_interval(
|
||||
std::time::Duration::from_millis(coords_response_interval_ms),
|
||||
),
|
||||
discovery_backoff: DiscoveryBackoff::with_params(backoff_base_secs, backoff_max_secs),
|
||||
discovery_backoff: DiscoveryBackoff::with_params(
|
||||
backoff_base_secs,
|
||||
backoff_max_secs,
|
||||
),
|
||||
discovery_forward_limiter: DiscoveryForwardRateLimiter::with_interval(
|
||||
std::time::Duration::from_secs(forward_min_interval_secs),
|
||||
),
|
||||
@@ -626,6 +623,7 @@ impl Node {
|
||||
config,
|
||||
state: NodeState::Created,
|
||||
is_leaf_only: false,
|
||||
node_profile: NodeProfile::Full,
|
||||
tree_state,
|
||||
bloom_state,
|
||||
coord_cache,
|
||||
@@ -654,8 +652,6 @@ impl Node {
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
@@ -685,6 +681,7 @@ impl Node {
|
||||
pub fn leaf_only(config: Config) -> Result<Self, NodeError> {
|
||||
let mut node = Self::new(config)?;
|
||||
node.is_leaf_only = true;
|
||||
node.node_profile = NodeProfile::Leaf;
|
||||
node.bloom_state = BloomState::leaf_only(*node.identity.node_addr());
|
||||
Ok(node)
|
||||
}
|
||||
@@ -712,19 +709,23 @@ impl Node {
|
||||
}
|
||||
|
||||
// Create Ethernet transport instances
|
||||
let eth_instances: Vec<_> = self
|
||||
.config
|
||||
.transports
|
||||
.ethernet
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
let xonly = self.identity.pubkey();
|
||||
for (name, eth_config) in eth_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let mut eth = EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
|
||||
eth.set_local_pubkey(xonly);
|
||||
transports.push(TransportHandle::Ethernet(eth));
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let eth_instances: Vec<_> = self
|
||||
.config
|
||||
.transports
|
||||
.ethernet
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
|
||||
let xonly = self.identity.pubkey();
|
||||
for (name, eth_config) in eth_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let mut eth = EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
|
||||
eth.set_local_pubkey(xonly);
|
||||
transports.push(TransportHandle::Ethernet(eth));
|
||||
}
|
||||
}
|
||||
|
||||
// Create TCP transport instances
|
||||
@@ -794,9 +795,7 @@ impl Node {
|
||||
#[cfg(any(not(feature = "ble"), test))]
|
||||
if !ble_instances.is_empty() {
|
||||
#[cfg(not(test))]
|
||||
tracing::warn!(
|
||||
"BLE transport configured but 'ble' feature not enabled at compile time"
|
||||
);
|
||||
tracing::warn!("BLE transport configured but 'ble' feature not enabled at compile time");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,9 +845,18 @@ impl Node {
|
||||
))
|
||||
})?;
|
||||
|
||||
// Parse the MAC address
|
||||
#[cfg(target_os = "linux")]
|
||||
let mac = crate::transport::ethernet::parse_mac_string(mac_str).map_err(|e| {
|
||||
NodeError::NoTransportForType(format!("invalid MAC in '{}': {}", addr_str, e))
|
||||
})?;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mac: [u8; 6] = {
|
||||
let _ = mac_str;
|
||||
return Err(NodeError::NoTransportForType(
|
||||
"Ethernet transport not available on this platform".into(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok((transport_id, TransportAddr::from_bytes(&mac)))
|
||||
}
|
||||
@@ -857,9 +865,13 @@ impl Node {
|
||||
/// (TransportId, TransportAddr) pair by finding the BLE transport
|
||||
/// instance matching the adapter name.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn resolve_ble_addr(&self, addr_str: &str) -> Result<(TransportId, TransportAddr), NodeError> {
|
||||
fn resolve_ble_addr(
|
||||
&self,
|
||||
addr_str: &str,
|
||||
) -> Result<(TransportId, TransportAddr), NodeError> {
|
||||
let ta = TransportAddr::from_string(addr_str);
|
||||
let adapter = crate::transport::ble::addr::adapter_from_addr(&ta).ok_or_else(|| {
|
||||
let adapter = crate::transport::ble::addr::adapter_from_addr(&ta)
|
||||
.ok_or_else(|| {
|
||||
NodeError::NoTransportForType(format!(
|
||||
"invalid BLE address format '{}': expected 'adapter/mac'",
|
||||
addr_str
|
||||
@@ -870,7 +882,9 @@ impl Node {
|
||||
let transport_id = self
|
||||
.transports
|
||||
.iter()
|
||||
.find(|(_, handle)| handle.transport_type().name == "ble" && handle.is_operational())
|
||||
.find(|(_, handle)| {
|
||||
handle.transport_type().name == "ble" && handle.is_operational()
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.ok_or_else(|| {
|
||||
NodeError::NoTransportForType(format!(
|
||||
@@ -987,6 +1001,22 @@ impl Node {
|
||||
self.is_leaf_only
|
||||
}
|
||||
|
||||
/// Get the node's profile (Full, NonRouting, Leaf).
|
||||
pub fn node_profile(&self) -> NodeProfile {
|
||||
self.node_profile
|
||||
}
|
||||
|
||||
/// Collect the set of peers that are not full nodes (non-routing/leaf).
|
||||
///
|
||||
/// Used by tree and routing functions to skip non-transit peers.
|
||||
fn non_full_peers(&self) -> std::collections::HashSet<NodeAddr> {
|
||||
self.peers
|
||||
.iter()
|
||||
.filter(|(_, p)| p.peer_profile() != NodeProfile::Full)
|
||||
.map(|(addr, _)| *addr)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// === Tree State ===
|
||||
|
||||
/// Get the tree state.
|
||||
@@ -1066,10 +1096,9 @@ impl Node {
|
||||
let now = std::time::Instant::now();
|
||||
let should_log = match self.last_mesh_size_log {
|
||||
None => true,
|
||||
Some(last) => {
|
||||
now.duration_since(last)
|
||||
>= std::time::Duration::from_secs(self.config.node.mmp.log_interval_secs)
|
||||
}
|
||||
Some(last) => now.duration_since(last) >= std::time::Duration::from_secs(
|
||||
self.config.node.mmp.log_interval_secs,
|
||||
),
|
||||
};
|
||||
if should_log {
|
||||
tracing::debug!(
|
||||
@@ -1118,6 +1147,7 @@ impl Node {
|
||||
self.tun_name.as_deref()
|
||||
}
|
||||
|
||||
|
||||
// === Resource Limits ===
|
||||
|
||||
/// Set the maximum number of connections (handshake phase).
|
||||
@@ -1198,17 +1228,14 @@ impl Node {
|
||||
/// Add a link.
|
||||
pub fn add_link(&mut self, link: Link) -> Result<(), NodeError> {
|
||||
if self.max_links > 0 && self.links.len() >= self.max_links {
|
||||
return Err(NodeError::MaxLinksExceeded {
|
||||
max: self.max_links,
|
||||
});
|
||||
return Err(NodeError::MaxLinksExceeded { max: self.max_links });
|
||||
}
|
||||
let link_id = link.link_id();
|
||||
let transport_id = link.transport_id();
|
||||
let remote_addr = link.remote_addr().clone();
|
||||
|
||||
self.links.insert(link_id, link);
|
||||
self.addr_to_link
|
||||
.insert((transport_id, remote_addr), link_id);
|
||||
self.addr_to_link.insert((transport_id, remote_addr), link_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1223,14 +1250,8 @@ impl Node {
|
||||
}
|
||||
|
||||
/// Find link ID by transport address.
|
||||
pub fn find_link_by_addr(
|
||||
&self,
|
||||
transport_id: TransportId,
|
||||
addr: &TransportAddr,
|
||||
) -> Option<LinkId> {
|
||||
self.addr_to_link
|
||||
.get(&(transport_id, addr.clone()))
|
||||
.copied()
|
||||
pub fn find_link_by_addr(&self, transport_id: TransportId, addr: &TransportAddr) -> Option<LinkId> {
|
||||
self.addr_to_link.get(&(transport_id, addr.clone())).copied()
|
||||
}
|
||||
|
||||
/// Remove a link.
|
||||
@@ -1376,14 +1397,11 @@ impl Node {
|
||||
pub(crate) fn register_identity(&mut self, node_addr: NodeAddr, pubkey: secp256k1::PublicKey) {
|
||||
let mut prefix = [0u8; 15];
|
||||
prefix.copy_from_slice(&node_addr.as_bytes()[0..15]);
|
||||
self.identity_cache
|
||||
.insert(prefix, (node_addr, pubkey, Self::now_ms()));
|
||||
self.identity_cache.insert(prefix, (node_addr, pubkey, Self::now_ms()));
|
||||
// LRU eviction
|
||||
let max = self.config.node.cache.identity_size;
|
||||
if self.identity_cache.len() > max
|
||||
&& let Some(oldest_key) = self
|
||||
.identity_cache
|
||||
.iter()
|
||||
&& let Some(oldest_key) = self.identity_cache.iter()
|
||||
.min_by_key(|(_, (_, _, ts))| *ts)
|
||||
.map(|(k, _)| *k)
|
||||
{
|
||||
@@ -1392,10 +1410,7 @@ impl Node {
|
||||
}
|
||||
|
||||
/// Look up a destination by FipsAddress prefix (bytes 1-15 of the IPv6 address).
|
||||
pub(crate) fn lookup_by_fips_prefix(
|
||||
&mut self,
|
||||
prefix: &[u8; 15],
|
||||
) -> Option<(NodeAddr, secp256k1::PublicKey)> {
|
||||
pub(crate) fn lookup_by_fips_prefix(&mut self, prefix: &[u8; 15]) -> Option<(NodeAddr, secp256k1::PublicKey)> {
|
||||
if let Some(entry) = self.identity_cache.get_mut(prefix) {
|
||||
entry.2 = Self::now_ms(); // LRU touch
|
||||
Some((entry.0, entry.1))
|
||||
@@ -1434,7 +1449,9 @@ impl Node {
|
||||
/// has declared us as their parent (making them our child).
|
||||
pub(crate) fn is_tree_peer(&self, peer_addr: &NodeAddr) -> bool {
|
||||
// Peer is our parent
|
||||
if !self.tree_state.is_root() && self.tree_state.my_declaration().parent_id() == peer_addr {
|
||||
if !self.tree_state.is_root()
|
||||
&& self.tree_state.my_declaration().parent_id() == peer_addr
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Peer is our child (their declaration names us as parent)
|
||||
@@ -1483,22 +1500,17 @@ impl Node {
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let dest_coords = self
|
||||
.coord_cache
|
||||
.get_and_touch(dest_node_addr, now_ms)?
|
||||
.clone();
|
||||
let dest_coords = self.coord_cache.get_and_touch(dest_node_addr, now_ms)?.clone();
|
||||
|
||||
// 3. Bloom filter candidates — requires dest_coords for loop-free selection.
|
||||
// If no candidate is strictly closer, fall through to tree routing.
|
||||
// 3. Bloom filter candidates — requires dest_coords for loop-free selection
|
||||
let candidates: Vec<&ActivePeer> = self.destination_in_filters(dest_node_addr);
|
||||
if !candidates.is_empty()
|
||||
&& let Some(peer) = self.select_best_candidate(&candidates, &dest_coords)
|
||||
{
|
||||
return Some(peer);
|
||||
if !candidates.is_empty() {
|
||||
return self.select_best_candidate(&candidates, &dest_coords);
|
||||
}
|
||||
|
||||
// 4. Greedy tree routing fallback
|
||||
let next_hop_id = self.tree_state.find_next_hop(&dest_coords)?;
|
||||
// 4. Greedy tree routing fallback (skip non-routing/leaf peers)
|
||||
let skip = self.non_full_peers();
|
||||
let next_hop_id = self.tree_state.find_next_hop(&dest_coords, &skip)?;
|
||||
|
||||
self.peers.get(&next_hop_id).filter(|p| p.can_send())
|
||||
}
|
||||
@@ -1558,9 +1570,15 @@ impl Node {
|
||||
best.map(|(peer, _, _)| peer)
|
||||
}
|
||||
|
||||
/// Check if a destination is in any peer's bloom filter.
|
||||
/// Check if a destination is in any full peer's bloom filter.
|
||||
///
|
||||
/// Skips non-routing and leaf peers (defensive — they shouldn't have
|
||||
/// filters claiming transit reachability, but guard against propagation bugs).
|
||||
pub fn destination_in_filters(&self, dest: &NodeAddr) -> Vec<&ActivePeer> {
|
||||
self.peers.values().filter(|p| p.may_reach(dest)).collect()
|
||||
self.peers
|
||||
.values()
|
||||
.filter(|p| p.peer_profile() == NodeProfile::Full && p.may_reach(dest))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the TUN packet sender channel.
|
||||
@@ -1588,8 +1606,7 @@ impl Node {
|
||||
node_addr: &NodeAddr,
|
||||
plaintext: &[u8],
|
||||
) -> Result<(), NodeError> {
|
||||
self.send_encrypted_link_message_with_ce(node_addr, plaintext, false)
|
||||
.await
|
||||
self.send_encrypted_link_message_with_ce(node_addr, plaintext, false).await
|
||||
}
|
||||
|
||||
/// Like `send_encrypted_link_message` but allows setting the FMP CE flag.
|
||||
@@ -1601,9 +1618,7 @@ impl Node {
|
||||
plaintext: &[u8],
|
||||
ce_flag: bool,
|
||||
) -> Result<(), NodeError> {
|
||||
let peer = self
|
||||
.peers
|
||||
.get_mut(node_addr)
|
||||
let peer = self.peers.get_mut(node_addr)
|
||||
.ok_or(NodeError::PeerNotFound(*node_addr))?;
|
||||
|
||||
let their_index = peer.their_index().ok_or_else(|| NodeError::SendFailed {
|
||||
@@ -1614,19 +1629,18 @@ impl Node {
|
||||
node_addr: *node_addr,
|
||||
reason: "no transport_id".into(),
|
||||
})?;
|
||||
let remote_addr = peer
|
||||
.current_addr()
|
||||
.cloned()
|
||||
.ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no current_addr".into(),
|
||||
})?;
|
||||
let remote_addr = peer.current_addr().cloned().ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no current_addr".into(),
|
||||
})?;
|
||||
|
||||
// Prepend 4-byte session-relative timestamp (inner header)
|
||||
let timestamp_ms = peer.session_elapsed_ms();
|
||||
|
||||
// MMP: read spin bit value before entering session borrow
|
||||
let sp_flag = peer.mmp().map(|mmp| mmp.spin_bit.tx_bit()).unwrap_or(false);
|
||||
let sp_flag = peer.mmp()
|
||||
.map(|mmp| mmp.spin_bit.tx_bit())
|
||||
.unwrap_or(false);
|
||||
let mut flags = if sp_flag { FLAG_SP } else { 0 };
|
||||
if ce_flag {
|
||||
flags |= FLAG_CE;
|
||||
@@ -1635,12 +1649,10 @@ impl Node {
|
||||
flags |= FLAG_KEY_EPOCH;
|
||||
}
|
||||
|
||||
let session = peer
|
||||
.noise_session_mut()
|
||||
.ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no noise session".into(),
|
||||
})?;
|
||||
let session = peer.noise_session_mut().ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no noise session".into(),
|
||||
})?;
|
||||
|
||||
// Inner plaintext: [timestamp:4 LE][msg_type][payload...]
|
||||
let inner_plaintext = prepend_inner_header(timestamp_ms, plaintext);
|
||||
@@ -1651,24 +1663,18 @@ impl Node {
|
||||
let header = build_established_header(their_index, counter, flags, payload_len);
|
||||
|
||||
// Encrypt with AAD binding to the outer header
|
||||
let ciphertext = session
|
||||
.encrypt_with_aad(&inner_plaintext, &header)
|
||||
.map_err(|e| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: format!("encryption failed: {}", e),
|
||||
})?;
|
||||
let ciphertext = session.encrypt_with_aad(&inner_plaintext, &header).map_err(|e| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: format!("encryption failed: {}", e),
|
||||
})?;
|
||||
|
||||
let wire_packet = build_encrypted(&header, &ciphertext);
|
||||
|
||||
// Re-borrow peer for stats update after sending
|
||||
let transport = self
|
||||
.transports
|
||||
.get(&transport_id)
|
||||
let transport = self.transports.get(&transport_id)
|
||||
.ok_or(NodeError::TransportNotFound(transport_id))?;
|
||||
|
||||
let bytes_sent = transport
|
||||
.send(&remote_addr, &wire_packet)
|
||||
.await
|
||||
let bytes_sent = transport.send(&remote_addr, &wire_packet).await
|
||||
.map_err(|e| match e {
|
||||
TransportError::MtuExceeded { packet_size, mtu } => NodeError::MtuExceeded {
|
||||
node_addr: *node_addr,
|
||||
|
||||
+9
-3
@@ -30,11 +30,15 @@ impl Node {
|
||||
/// Send a TreeAnnounce to a specific peer, respecting rate limits.
|
||||
///
|
||||
/// If the peer is rate-limited, the announce is marked pending for
|
||||
/// delivery on the next tick cycle.
|
||||
/// delivery on the next tick cycle. Leaf nodes do not send tree
|
||||
/// announces (they don't participate in the spanning tree).
|
||||
pub(super) async fn send_tree_announce_to_peer(
|
||||
&mut self,
|
||||
peer_addr: &NodeAddr,
|
||||
) -> Result<(), NodeError> {
|
||||
if self.node_profile == crate::protocol::NodeProfile::Leaf {
|
||||
return Ok(());
|
||||
}
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
@@ -221,7 +225,8 @@ impl Node {
|
||||
.filter(|(_, peer)| peer.has_srtt())
|
||||
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
||||
.collect();
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
||||
let skip = self.non_full_peers();
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs, &skip) {
|
||||
let new_seq = self.tree_state.my_declaration().sequence() + 1;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -372,7 +377,8 @@ impl Node {
|
||||
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
||||
.collect();
|
||||
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
||||
let skip = self.non_full_peers();
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs, &skip) {
|
||||
let new_seq = self.tree_state.my_declaration().sequence() + 1;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
Reference in New Issue
Block a user