/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 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::{Duration, Instant, UNIX_EPOCH},
};
use log::{debug, warn};
use url::Url;
use super::{
super::p2p::{P2p, P2pPtr},
store::HostColor,
};
use crate::{
net::{
connector::Connector, hosts::store::HostState, protocol::ProtocolVersion, session::Session,
},
system::{
run_until_completion, sleep, timeout::timeout, LazyWeak, StoppableTask, StoppableTaskPtr,
},
Error,
};
pub type GreylistRefineryPtr = Arc;
/// Probe random peers on the greylist. If a peer is responsive, update the last_seen field and
/// add it to the whitelist. If a node does not respond, remove it from the greylist.
/// Called periodically.
pub struct GreylistRefinery {
/// Weak pointer to parent p2p object
pub(in crate::net) p2p: LazyWeak,
process: StoppableTaskPtr,
}
impl GreylistRefinery {
pub fn new() -> Arc {
Arc::new(Self { p2p: LazyWeak::new(), process: StoppableTask::new() })
}
pub async fn start(self: Arc) {
match self.p2p().hosts().container.load_all(&self.p2p().settings().hostlist).await {
Ok(()) => {
debug!(target: "net::refinery::start()", "Load hosts successful!");
}
Err(e) => {
warn!(target: "net::refinery::start()", "Error loading hosts {}", e);
}
}
let ex = self.p2p().executor();
self.process.clone().start(
async move {
//self.listen_for_channels().await;
self.run().await;
unreachable!();
},
// Ignore stop handler
|_| async {},
Error::NetworkServiceStopped,
ex,
);
}
pub async fn stop(self: Arc) {
self.process.stop().await;
match self.p2p().hosts().container.save_all(&self.p2p().settings().hostlist).await {
Ok(()) => {
debug!(target: "net::refinery::stop()", "Save hosts successful!");
}
Err(e) => {
warn!(target: "net::refinery::stop()", "Error saving hosts {}", e);
}
}
}
// Randomly select a peer on the greylist and probe it.
// This method will remove from the greylist and store on the whitelist
// providing the peer is responsive.
async fn run(self: Arc) {
let settings = self.p2p().settings();
let hosts = self.p2p().hosts();
loop {
sleep(settings.greylist_refinery_interval).await;
if hosts.container.is_empty(HostColor::Grey).await {
debug!(target: "net::refinery",
"Greylist is empty! Cannot start refinery process");
continue
}
// Pause the refinery if we've had zero connections for longer than the configured
// limit.
let offline_limit = Duration::from_secs(settings.time_with_no_connections);
let offline_timer = Instant::now().duration_since(*hosts.last_connection.read().await);
if hosts.channels().await.is_empty() && offline_timer >= offline_limit {
warn!(target: "net::refinery", "No connections for {}s. Refinery paused.",
offline_timer.as_secs());
// It is neccessary to clear suspended hosts at this point, otherwise these
// hosts cannot be connected to in Outbound Session. Failure to do this could
// result in the refinery being paused forver (since connections could never be
// made).
let suspended_hosts = hosts.suspended().await;
for host in suspended_hosts {
hosts.unregister(&host).await;
}
continue
}
// Only attempt to refine peers that match our transports.
match hosts
.container
.fetch_random_with_schemes(HostColor::Grey, &settings.allowed_transports)
.await
{
Some((entry, position)) => {
let url = &entry.0;
if hosts.try_register(url.clone(), HostState::Refine).await.is_err() {
continue
}
if !ping_node(url.clone(), self.p2p().clone()).await {
hosts.container.remove(HostColor::Grey, url, position).await;
debug!(
target: "net::refinery",
"Peer {} is non-responsive. Removed from greylist", url,
);
// Remove this entry from HostRegistry to avoid this host getting
// stuck in the Refining state.
//
// It is not necessary to call this when the refinery passes, since the
// state will be changed to Connected.
hosts.unregister(url).await;
continue
}
let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs();
// Append to the whitelist.
hosts.container.store_or_update(HostColor::White, url.clone(), last_seen).await;
// Remove whitelisted peer from the greylist.
hosts.container.remove(HostColor::Grey, url, position).await;
}
None => {
debug!(target: "net::refinery", "No matching greylist entries found. Cannot proceed with refinery");
continue
}
}
}
}
fn p2p(&self) -> P2pPtr {
self.p2p.upgrade()
}
}
/// Check a node is online by establishing a channel with it and conducting a handshake with a
/// version exchange.
///
/// We must use run_until_completion() to ensure this code will complete even if the parent task
/// has been destroyed. Otherwise ping_node() will become a zombie process if the rest of the p2p
/// network has been shutdown but the handshake it still ongoing.
///
/// Other parts of the p2p stack have safe shutdown methods built into them due to the ownership
/// structure. Here we are creating a outbound session that is not owned by anything and is not
/// so is not safely cancelled on shutdown.
pub async fn ping_node(addr: Url, p2p: P2pPtr) -> bool {
let ex = p2p.executor();
run_until_completion(ping_node_impl(addr.clone(), p2p), ex).await
}
async fn ping_node_impl(addr: Url, p2p: P2pPtr) -> bool {
let session_outbound = p2p.session_outbound();
let parent = Arc::downgrade(&session_outbound);
let connector = Connector::new(p2p.settings(), parent);
debug!(target: "net::refinery::ping_node()", "Attempting to connect to {}", addr);
match connector.connect(&addr).await {
Ok((url, channel)) => {
debug!(target: "net::refinery::ping_node()", "Successfully created a channel with {}", url);
// First initialize the version protocol and its Version, Verack subscribers.
let proto_ver = ProtocolVersion::new(channel.clone(), p2p.settings()).await;
debug!(target: "net::refinery::ping_node()", "Performing handshake protocols with {}", url);
// Then run the version exchange, store the channel and subscribe to a stop signal.
let handshake_task = session_outbound.perform_handshake_protocols(
proto_ver,
channel.clone(),
p2p.executor(),
);
debug!(target: "net::refinery::ping_node()", "Starting channel {}", url);
channel.clone().start(p2p.executor());
// Ensure the channel gets stopped by adding a timeout to the handshake. Otherwise if
// the handshake does not finish channel.stop() will never get called, resulting in
// zombie processes.
let result = timeout(Duration::from_secs(5), handshake_task).await;
debug!(target: "net::refinery::ping_node()", "Stopping channel {}", url);
channel.stop().await;
match result {
Ok(_) => {
debug!(target: "net::refinery::ping_node()", "Handshake success!");
true
}
Err(e) => {
debug!(target: "net::refinery::ping_node()", "Handshake err: {}", e);
false
}
}
}
Err(e) => {
debug!(target: "net::refinery::ping_node()", "Failed to connect to {}, ({})", addr, e);
false
}
}
}