/* 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 .
*/
//! `RefineSession` manages the `GreylistRefinery`, which randomly selects
//! entries on the greylist and updates them to whitelist if active,
//!
//! `GreylistRefinery` makes use of a `RefineSession` method called
//! `handshake_node()`, which uses a `Connector` to establish a `Channel` with
//! a provided address, and then does a version exchange across the channel
//! (`perform_handshake_protocols`). `handshake_node()` can either succeed,
//! fail, or timeout.
use futures::{
future::{select, Either},
pin_mut,
};
use smol::Timer;
use std::{
sync::Arc,
time::{Duration, Instant, UNIX_EPOCH},
};
use async_trait::async_trait;
use log::{debug, warn};
use url::Url;
use super::super::p2p::{P2p, P2pPtr};
use crate::{
net::{
connector::Connector,
hosts::{HostColor, HostState},
protocol::ProtocolVersion,
session::{Session, SessionBitFlag, SESSION_REFINE},
},
system::{sleep, LazyWeak, StoppableTask, StoppableTaskPtr},
Error,
};
pub type RefineSessionPtr = Arc;
pub struct RefineSession {
/// Weak pointer to parent p2p object
pub(in crate::net) p2p: LazyWeak,
/// Task that periodically checks entries in the greylist.
pub(in crate::net) refinery: Arc,
}
impl RefineSession {
pub fn new() -> RefineSessionPtr {
let self_ = Arc::new(Self { p2p: LazyWeak::new(), refinery: GreylistRefinery::new() });
self_.refinery.session.init(self_.clone());
self_
}
/// Start the refinery and self handshake processes.
pub(crate) async fn start(self: Arc) {
match self.p2p().hosts().container.load_all(&self.p2p().settings().hostlist) {
Ok(()) => {
debug!(target: "net::refine_session::start()", "Load hosts successful!");
}
Err(e) => {
warn!(target: "net::refine_session::start()", "Error loading hosts {}", e);
}
}
match self.p2p().hosts().import_blacklist() {
Ok(()) => {
debug!(target: "net::refine_session::start()", "Import blacklist successful!");
}
Err(e) => {
warn!(target: "net::refine_session::start()",
"Error importing blacklist from config file {}", e);
}
}
debug!(target: "net::refine_session", "Starting greylist refinery process");
self.refinery.clone().start().await;
}
/// Stop the refinery and self handshake processes.
pub(crate) async fn stop(&self) {
debug!(target: "net::refine_session", "Stopping refinery process");
self.refinery.clone().stop().await;
match self.p2p().hosts().container.save_all(&self.p2p().settings().hostlist) {
Ok(()) => {
debug!(target: "net::refine_session::stop()", "Save hosts successful!");
}
Err(e) => {
warn!(target: "net::refine_session::stop()", "Error saving hosts {}", e);
}
}
}
/// Globally accessible function to perform a version exchange with a
/// given address. Returns `true` if an address is accessible, false
/// otherwise.
pub async fn handshake_node(self: Arc, addr: Url, p2p: P2pPtr) -> bool {
let self_ = Arc::downgrade(&self);
let connector = Connector::new(self.p2p().settings(), self_);
debug!(target: "net::refinery::handshake_node()", "Attempting to connect to {}", addr);
match connector.connect(&addr).await {
Ok((url, channel)) => {
debug!(target: "net::refinery::handshake_node()", "Successfully created a channel with {}", url);
// First initialize the version protocol and its Version, Verack subscriptions.
let proto_ver = ProtocolVersion::new(channel.clone(), p2p.settings()).await;
debug!(target: "net::refinery::handshake_node()", "Performing handshake protocols with {}", url);
// Then run the version exchange, store the channel and subscribe to a stop signal.
let handshake =
self.perform_handshake_protocols(proto_ver, channel.clone(), p2p.executor());
debug!(target: "net::refinery::handshake_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 timeout = Timer::after(Duration::from_secs(5));
pin_mut!(timeout);
pin_mut!(handshake);
let result = match select(handshake, timeout).await {
Either::Left((Ok(_), _)) => {
debug!(target: "net::refinery::handshake_node()", "Handshake success!");
true
}
Either::Left((Err(e), _)) => {
debug!(target: "net::refinery::handshake_node()", "Handshake error={}", e);
false
}
Either::Right((_, _)) => {
debug!(target: "net::refinery::handshake_node()", "Handshake timed out");
false
}
};
debug!(target: "net::refinery::handshake_node()", "Stopping channel {}", url);
channel.stop().await;
result
}
Err(e) => {
debug!(target: "net::refinery::handshake_node()", "Failed to connect to {}, ({})", addr, e);
false
}
}
}
}
#[async_trait]
impl Session for RefineSession {
fn p2p(&self) -> P2pPtr {
self.p2p.upgrade()
}
fn type_id(&self) -> SessionBitFlag {
SESSION_REFINE
}
}
/// Periodically probes entries in the greylist.
///
/// Randomly selects a greylist entry and tries to establish a local
/// connection to it using the method handshake_node(), which creates a
/// channel and does a version exchange using `perform_handshake_protocols()`.
///
/// If successful, the entry is removed from the greylist and added to the
/// whitelist with an updated last_seen timestamp. If non-successful, the
/// entry is removed from the greylist.
pub struct GreylistRefinery {
/// Weak pointer to parent object
session: LazyWeak,
process: StoppableTaskPtr,
}
impl GreylistRefinery {
pub fn new() -> Arc {
Arc::new(Self { session: LazyWeak::new(), process: StoppableTask::new() })
}
pub async fn start(self: Arc) {
let ex = self.p2p().executor();
self.process.clone().start(
async move {
self.run().await;
unreachable!();
},
// Ignore stop handler
|_| async {},
Error::NetworkServiceStopped,
ex,
);
}
pub async fn stop(self: Arc) {
self.process.stop().await;
}
// 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 p2p = self.p2p();
let hosts = p2p.hosts();
let settings = p2p.settings();
loop {
sleep(settings.greylist_refinery_interval).await;
if hosts.container.is_empty(HostColor::Grey) {
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.lock().unwrap()) };
if !p2p.is_connected() && offline_timer >= offline_limit {
warn!(target: "net::refinery", "No connections for {}s. GreylistRefinery 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();
for host in suspended_hosts {
hosts.unregister(&host);
}
continue
}
// Only attempt to refine peers that match our transports.
match hosts
.container
.fetch_random_with_schemes(HostColor::Grey, &settings.allowed_transports)
{
Some((entry, _)) => {
let url = &entry.0;
if let Err(e) = hosts.try_register(url.clone(), HostState::Refine) {
debug!(target: "net::refinery", "Unable to refine addr={}, err={}",
url.clone(), e);
continue
}
if !self.session().handshake_node(url.clone(), p2p.clone()).await {
hosts.container.remove_if_exists(HostColor::Grey, url);
debug!(
target: "net::refinery",
"Peer {} handshake failed. Removed from greylist", url,
);
// Remove this entry from HostRegistry to avoid this host getting
// stuck in the Refining state. This is a safe since the hostlist
// modification is now complete.
hosts.unregister(url);
continue
}
debug!(
target: "net::refinery",
"Peer {} handshake successful. Adding to whitelist", url,
);
let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs();
// Add to the whitelist and remove from the greylist.
hosts.move_host(url, last_seen, HostColor::White).unwrap();
// When move is complete we can safely stop tracking this peer.
hosts.unregister(url);
debug!(target: "net::refinery", "GreylistRefinery complete!");
continue
}
None => {
debug!(target: "net::refinery", "No matching greylist entries found. Cannot proceed with refinery");
continue
}
}
}
}
fn session(&self) -> RefineSessionPtr {
self.session.upgrade()
}
fn p2p(&self) -> P2pPtr {
self.session().p2p()
}
}