/* 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 .
*/
//! TODO: doc
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant, UNIX_EPOCH},
};
use async_trait::async_trait;
use log::{debug, warn};
use smol::lock::Mutex;
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, timeout::timeout, 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,
/// Task that periodically checks our external addresses.
pub(in crate::net) self_handshake: Arc,
}
impl RefineSession {
pub fn new() -> RefineSessionPtr {
let self_ = Arc::new(Self {
p2p: LazyWeak::new(),
refinery: GreylistRefinery::new(),
self_handshake: SelfHandshake::new(),
});
self_.self_handshake.session.init(self_.clone());
self_.refinery.session.init(self_.clone());
self_
}
pub(crate) async fn start(self: Arc) {
debug!(target: "net::refine_session", "Starting greylist refinery process");
self.refinery.clone().start().await;
debug!(target: "net::refine_session", "Starting self handshake process");
self.self_handshake.clone().start().await;
}
pub(crate) async fn stop(&self) {
debug!(target: "net::refine_session", "Stopping refinery process");
self.refinery.clone().stop().await;
debug!(target: "net::refine_session", "Stopping self handshake process");
self.self_handshake.clone().stop().await;
}
// TODO: doc and explain why it's public
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 subscribers.
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_task =
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 result = timeout(Duration::from_secs(5), handshake_task).await;
debug!(target: "net::refinery::handshake_node()", "Stopping channel {}", url);
channel.stop().await;
match result {
Ok(_) => {
debug!(target: "net::refinery::handshake_node()", "Handshake success!");
true
}
Err(e) => {
debug!(target: "net::refinery::handshake_node()", "Handshake err: {}", e);
false
}
}
}
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) {
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.run().await;
unreachable!();
},
// Ignore stop handler
|_| async {},
Error::NetworkServiceStopped,
ex,
);
}
pub async fn stop(self: Arc) {
debug!(target: "net::refinery", "Stopping refinery");
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. 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().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 let Err(e) = hosts.try_register(url.clone(), HostState::Refine).await {
debug!(target: "net::refinery", "Unable to refine addr={}, err={}",
url.clone(), e);
continue
}
if !self.session().handshake_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
}
debug!(
target: "net::refinery",
"Peer {} is responsive. 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).await.unwrap();
hosts.unregister(url).await;
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()
}
}
/// Periodically try to do a version exchange with our own external
/// addresses. If the version exchange is successful, take a timestamp and
/// save it along with the external addresses. Each address along with its
/// timestamp (the `last_seen` data field) is sent in to other nodes in
/// ProtocolAddr and ProtocolSeed.
///
/// On first run, SelfHandshake will immediately conduct a version exchange
/// with our external addresses, and if successful update the last_seen
/// field. The process will wait [TODO: self_handshake_interval) before retrying.
///
/// There are two situations in which this can fail:
///
/// 1. If our external address is misconfigured
/// 2. If we have reached our inbound connection limit.
///
/// If our external address is misconfigured, doing a version exchange
/// with ourselves will not work and so the external addresses will not
/// be shared with other nodes.
///
/// If we have reached our inbound connection limit, the external address
/// will continue to be broadcast with an older `last_seen` (from before
/// our inbound connection was reached).
pub struct SelfHandshake {
process: StoppableTaskPtr,
session: LazyWeak,
pub(in crate::net) addrs: Mutex>,
}
impl SelfHandshake {
fn new() -> Arc {
Arc::new(Self {
process: StoppableTask::new(),
session: LazyWeak::new(),
addrs: Mutex::new(HashMap::new()),
})
}
async fn start(self: Arc) {
let ex = self.session().p2p().executor();
self.process.clone().start(
async move {
self.run().await;
unreachable!();
},
// Ignore stop handler
|_| async {},
Error::NetworkServiceStopped,
ex,
);
}
async fn stop(self: Arc) {
self.process.stop().await
}
async fn run(self: Arc) {
let external_addrs = self.session().p2p().settings().external_addrs.clone();
let mut current_attempt = 0;
loop {
if current_attempt >= 1 {
// TODO: make this a configurable interval
sleep(600).await;
}
// Only proceed if the external address is configured.
if external_addrs.is_empty() {
current_attempt += 1;
continue
}
for addr in external_addrs.iter() {
debug!(target: "net::refine_session::self_handshake",
"Attempting a version exchange addr={}", addr);
if self.session().handshake_node(addr.clone(), self.session().p2p()).await {
debug!(target: "net::refine_session::self_handshake",
"Version exchange successful! Updating last seen addr={}", addr);
let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs();
let mut addrs = self.addrs.lock().await;
if addrs.contains_key(addr) {
let val = addrs.get_mut(addr).unwrap();
*val = last_seen;
}
addrs.insert(addr.clone(), last_seen);
} else {
// Either our external addr is invalid or our max inbound
// connection count has been reached.
warn!(target: "net::refine_session::self_handshake",
"Version exchange failed! addr={}", addr);
}
}
current_attempt += 1;
}
}
fn session(&self) -> RefineSessionPtr {
self.session.upgrade()
}
}