/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2025 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{sync::Arc, time::UNIX_EPOCH}; use tracing::{error, info, warn}; use crate::{ dht::{event::DhtEvent, ChannelCacheItem, DhtHandler, DhtNode, SESSION_MANUAL}, net::{ hosts::HostColor, session::{SESSION_DIRECT, SESSION_INBOUND, SESSION_OUTBOUND}, }, system::sleep, util::time::Timestamp, Result, }; /// Handle DHT events. pub async fn events_task(handler: Arc) -> Result<()> { let dht = handler.dht(); let sub = dht.event_publisher.clone().subscribe().await; loop { let event = sub.receive().await; match event { // On [`DhtEvent::PingReceived`] set channel_cache.ping_received = true DhtEvent::PingReceived { from, .. } => { let channel_cache_lock = dht.channel_cache.clone(); let mut channel_cache = channel_cache_lock.write().await; if let Some(cached) = channel_cache.get_mut(&from.info.id) { cached.ping_received = true; } } // On [`DhtEvent::PingSent`] set channel_cache.ping_sent = true DhtEvent::PingSent { to, .. } => { let channel_cache_lock = dht.channel_cache.clone(); let mut channel_cache = channel_cache_lock.write().await; if let Some(cached) = channel_cache.get_mut(&to.info.id) { cached.ping_sent = true; } } _ => {} } } } /// Send a DHT ping request when there is a new channel, to know the node id of the new peer, /// Then fill the channel cache and the buckets pub async fn channel_task(handler: Arc) -> Result<()> { let dht = handler.dht(); let p2p = dht.p2p.clone(); let channel_sub = p2p.hosts().subscribe_channel().await; loop { let res = channel_sub.receive().await; if res.is_err() { continue; } let channel = res.unwrap(); let channel_cache_lock = dht.channel_cache.clone(); let mut channel_cache = channel_cache_lock.write().await; // Skip this channel if it's not new if channel_cache.keys().any(|&k| k == channel.info.id) { continue; } channel_cache.insert( channel.info.id, ChannelCacheItem { node: None, last_used: Timestamp::current_time(), ping_received: false, ping_sent: false, }, ); drop(channel_cache); // It's a manual connection if channel.session_type_id() & SESSION_MANUAL != 0 { let ping_res = dht.ping(channel.clone()).await; if let Err(e) = ping_res { warn!(target: "dht::channel_task", "Error while pinging manual connection (requesting node id) {}: {e}", channel.display_address()); continue; } } // It's an outbound connection if channel.session_type_id() & SESSION_OUTBOUND != 0 { let _ = dht.ping(channel.clone()).await; continue; } // It's a direct connection if channel.session_type_id() & SESSION_DIRECT != 0 { p2p.session_direct().inc_channel_usage(&channel, 1).await; let _ = dht.ping(channel.clone()).await; dht.cleanup_channel(channel).await; continue; } } } /// Periodically send a DHT ping to known hosts. If the ping is successful, we /// move the host to the whitelist (updating the last seen field). /// /// This is necessary to prevent unresponsive nodes staying on the whitelist, /// as the DHT does not require any outbound slot. pub async fn dht_refinery_task(handler: Arc) -> Result<()> { let interval = 60; // TODO: Make a setting let min_ping_interval = 10 * 60; // TODO: Make a setting let dht = handler.dht(); let hosts = dht.p2p.hosts(); loop { let mut hostlist = hosts.container.fetch_all(HostColor::Gold); hostlist.extend(hosts.container.fetch_all(HostColor::White)); // Include the greylist only if the DHT is not bootstrapped yet if !handler.dht().is_bootstrapped().await { hostlist.extend(hosts.container.fetch_all(HostColor::Grey)); } for entry in &hostlist { let url = &entry.0; let host_cache = dht.host_cache.read().await; let last_ping = host_cache.get(url).map(|h| h.last_ping.inner()); if last_ping.is_some() && last_ping.unwrap() > Timestamp::current_time().inner() - min_ping_interval { continue } drop(host_cache); let res = dht.create_channel(url).await; if res.is_err() { continue } let (channel, _) = res.unwrap(); dht.cleanup_channel(channel).await; let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs(); if let Err(e) = hosts.whitelist_host(url, last_seen).await { error!(target: "dht::tasks::whitelist_refinery_task", "Could not send {url} to the whitelist: {e}"); } break } match hostlist.is_empty() { true => sleep(5).await, false => sleep(interval).await, } } } /// Add a node to the DHT buckets. /// If the bucket is already full, we ping the least recently seen node in the /// bucket: if successful it becomes the most recently seen node, if the ping /// fails we remove it and add the new node. /// [`Dht::update_node()`] increments a channel's usage count (in the direct /// session) and triggers this task. This task decrements the usage count /// using [`Dht::cleanup_channel()`]. pub async fn add_node_task(handler: Arc) -> Result<()> { let dht = handler.dht(); loop { let (node, channel) = dht.add_node_rx.recv().await.unwrap(); let self_node = handler.node().await; if self_node.is_err() { continue; } let self_node = self_node.unwrap(); let bucket_index = dht.get_bucket_index(&self_node.id(), &node.id()).await; let buckets_lock = dht.buckets.clone(); let mut buckets = buckets_lock.write().await; let bucket = &mut buckets[bucket_index]; // Do not add ourselves to the buckets if node.id() == self_node.id() { dht.cleanup_channel(channel).await; continue; } // Don't add this node if it has any external address that is the same as one of ours let node_addresses = node.addresses(); if self_node.addresses().iter().any(|addr| node_addresses.contains(addr)) { dht.cleanup_channel(channel).await; continue; } // Do not add a node to the buckets if it does not have an address if node.addresses().is_empty() { dht.cleanup_channel(channel).await; continue; } // We already have this node, move it to the tail of the bucket if let Some(node_index) = bucket.nodes.iter().position(|n| n.id() == node.id()) { bucket.nodes.remove(node_index); bucket.nodes.push(node); dht.cleanup_channel(channel).await; continue; } // Bucket is full if bucket.nodes.len() >= dht.settings.k { // Ping the least recently seen node if let Ok((channel2, node)) = dht.get_channel(&bucket.nodes[0]).await { // Ping was successful, move the least recently seen node to the tail let n = bucket.nodes.remove(0); bucket.nodes.push(n); drop(buckets); dht.on_new_node(&node.clone(), channel2.clone()).await; dht.cleanup_channel(channel2).await; dht.cleanup_channel(channel).await; continue; } // Ping was not successful, remove the least recently seen node and add the new node bucket.nodes.remove(0); bucket.nodes.push(node.clone()); drop(buckets); dht.on_new_node(&node.clone(), channel.clone()).await; dht.cleanup_channel(channel).await; continue; } // Bucket is not full, just add the node bucket.nodes.push(node.clone()); drop(buckets); dht.on_new_node(&node.clone(), channel.clone()).await; dht.cleanup_channel(channel).await; } } /// Close inbound connections that are unused for too long. pub async fn disconnect_inbounds_task(handler: Arc) -> Result<()> { let interval = 10; // TODO: Make a setting let dht = handler.dht(); loop { sleep(interval).await; let min_last_used = Timestamp::current_time().inner() - dht.settings.inbound_timeout; let channel_cache_lock = dht.channel_cache.clone(); let mut channel_cache = channel_cache_lock.write().await; for (channel_id, cached) in channel_cache.clone() { // Check that: // The channel timed out, if cached.last_used.inner() >= min_last_used { continue; } // The channel exists, let channel = dht.p2p.get_channel(channel_id); if channel.is_none() { channel_cache.remove(&channel_id); continue; } let channel = channel.unwrap(); // And the channel is inbound. if channel.session_type_id() & SESSION_INBOUND == 0 { continue; } // Now we can stop it and remove it from the channel cache info!(target: "dht::disconnect_inbounds_task", "Closing expired inbound channel [{}]", channel.display_address()); channel.stop().await; channel_cache.remove(&channel.info.id); } } } /// Removes entries from [`crate::dht::Dht::channel_cache`] when a channel is /// stopped. pub async fn cleanup_channels_task(handler: Arc) -> Result<()> { let interval = 60; // TODO: Make a setting let dht = handler.dht(); loop { sleep(interval).await; let channel_cache_lock = dht.channel_cache.clone(); let mut channel_cache = channel_cache_lock.write().await; for (channel_id, _) in channel_cache.clone() { match dht.p2p.get_channel(channel_id) { Some(channel) => { if channel.is_stopped() { channel_cache.remove(&channel_id); } } None => { channel_cache.remove(&channel_id); } } } } }