/* 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() } }