//! The Discovery v5 protocol. See the module level docs for further details. //! //! This provides the main struct for running and interfacing with a discovery v5 server. //! //! A [`Discv5`] struct needs to be created either with an [`crate::executor::Executor`] specified in the //! [`Discv5Config`] via the [`crate::Discv5ConfigBuilder`] or in the presence of a tokio runtime that has //! timing and io enabled. //! //! Once a [`Discv5`] struct has been created the service is started by running the [`Discv5::start`] //! functions with a UDP socket. This will start a discv5 server in the background listening on the //! specified UDP socket. //! //! The server can be shutdown using the [`Discv5::shutdown`] function. use crate::{ error::{Discv5Error, QueryError, RequestError}, kbucket::{ self, ConnectionDirection, ConnectionState, FailureReason, InsertResult, KBucketsTable, NodeStatus, UpdateResult, }, node_info::NodeContact, service::{QueryKind, Service, ServiceRequest, TalkRequest}, Discv5Config, Enr, }; use enr::{CombinedKey, EnrError, EnrKey, NodeId}; use parking_lot::RwLock; use std::{ future::Future, net::SocketAddr, sync::Arc, time::{Duration, Instant}, }; use tokio::sync::{mpsc, oneshot}; use tracing::{debug, warn}; #[cfg(feature = "libp2p")] use {libp2p_core::Multiaddr, std::convert::TryFrom}; // Create lazy static variable for the global permit/ban list use crate::metrics::{Metrics, METRICS}; lazy_static! { pub static ref PERMIT_BAN_LIST: RwLock = RwLock::new(crate::PermitBanList::default()); } mod test; /// Events that can be produced by the `Discv5` event stream. #[derive(Debug)] pub enum Discv5Event { /// A node has been discovered from a FINDNODES request. /// /// The ENR of the node is returned. Various properties can be derived from the ENR. /// This happen spontaneously through queries as nodes return ENR's. These ENR's are not /// guaranteed to be live or contactable. Discovered(Enr), /// A new ENR was added to the routing table. EnrAdded { enr: Enr, replaced: Option }, /// A new node has been added to the routing table. NodeInserted { node_id: NodeId, replaced: Option, }, /// Our local ENR IP address has been updated. SocketUpdated(SocketAddr), /// A node has initiated a talk request. TalkRequest(TalkRequest), } /// The main Discv5 Service struct. This provides the user-level API for performing queries and /// interacting with the underlying service. pub struct Discv5 { config: Discv5Config, /// The channel to make requests from the main service. service_channel: Option>, /// The exit channel to shutdown the underlying service. service_exit: Option>, /// The routing table of the discv5 service. kbuckets: Arc>>, /// The local ENR of the server. local_enr: Arc>, /// The key associated with the local ENR, required for updating the local ENR. enr_key: Arc>, } impl Discv5 { pub fn new( local_enr: Enr, enr_key: CombinedKey, mut config: Discv5Config, ) -> Result { // ensure the keypair matches the one that signed the enr. if local_enr.public_key() != enr_key.public() { return Err("Provided keypair does not match the provided ENR"); } // If an executor is not provided, assume a current tokio runtime is running. If not panic. if config.executor.is_none() { config.executor = Some(Box::new(crate::executor::TokioExecutor::default())); }; // NOTE: Currently we don't expose custom filter support in the configuration. Users can // optionally use the IP filter via the ip_limit configuration parameter. In the future, we // may expose this functionality to the users if there is demand for it. let (table_filter, bucket_filter) = if config.ip_limit { ( Some(Box::new(kbucket::IpTableFilter) as Box>), Some(Box::new(kbucket::IpBucketFilter) as Box>), ) } else { (None, None) }; let local_enr = Arc::new(RwLock::new(local_enr)); let enr_key = Arc::new(RwLock::new(enr_key)); let kbuckets = Arc::new(RwLock::new(KBucketsTable::new( local_enr.read().node_id().into(), Duration::from_secs(60), config.incoming_bucket_limit, table_filter, bucket_filter, ))); // Update the PermitBan list based on initial configuration *PERMIT_BAN_LIST.write() = config.permit_ban_list.clone(); Ok(Discv5 { config, service_channel: None, service_exit: None, kbuckets, local_enr, enr_key, }) } /// Starts the required tasks and begins listening on a given UDP SocketAddr. pub async fn start(&mut self, listen_socket: SocketAddr) -> Result<(), Discv5Error> { if self.service_channel.is_some() { warn!("Service is already started"); return Err(Discv5Error::ServiceAlreadyStarted); } // create the main service let (service_exit, service_channel) = Service::spawn( self.local_enr.clone(), self.enr_key.clone(), self.kbuckets.clone(), self.config.clone(), listen_socket, ) .await?; self.service_exit = Some(service_exit); self.service_channel = Some(service_channel); Ok(()) } /// Terminates the service. pub fn shutdown(&mut self) { if let Some(exit) = self.service_exit.take() { if exit.send(()).is_err() { debug!("Discv5 service already shutdown"); } self.service_channel = None; } else { debug!("Service is already shutdown"); } } /// Adds a known ENR of a peer participating in Service to the /// routing table. /// /// This allows pre-populating the Kademlia routing table with known /// addresses, so that they can be used immediately in following DHT /// operations involving one of these peers, without having to dial /// them upfront. pub fn add_enr(&self, enr: Enr) -> Result<(), &'static str> { // only add ENR's that have a valid udp socket. if enr.udp_socket().is_none() { warn!("ENR attempted to be added without a UDP socket has been ignored"); return Err("ENR has no UDP socket to connect to"); } if !(self.config.table_filter)(&enr) { warn!("ENR attempted to be added which is banned by the configuration table filter."); return Err("ENR banned by table filter"); } let key = kbucket::Key::from(enr.node_id()); match self.kbuckets.write().insert_or_update( &key, enr, NodeStatus { state: ConnectionState::Disconnected, direction: ConnectionDirection::Incoming, }, ) { InsertResult::Inserted | InsertResult::Pending { .. } | InsertResult::StatusUpdated { .. } | InsertResult::ValueUpdated | InsertResult::Updated { .. } | InsertResult::UpdatedPending => Ok(()), InsertResult::Failed(FailureReason::BucketFull) => Err("Table full"), InsertResult::Failed(FailureReason::BucketFilter) => Err("Failed bucket filter"), InsertResult::Failed(FailureReason::TableFilter) => Err("Failed table filter"), InsertResult::Failed(FailureReason::InvalidSelfUpdate) => Err("Invalid self update"), InsertResult::Failed(_) => Err("Failed to insert ENR"), } } /// Removes a `node_id` from the routing table. /// /// This allows applications, for whatever reason, to remove nodes from the local routing /// table. Returns `true` if the node was in the table and `false` otherwise. pub fn remove_node(&self, node_id: &NodeId) -> bool { let key = &kbucket::Key::from(*node_id); self.kbuckets.write().remove(key) } /// Returns a vector of closest nodes by the given distances. pub fn nodes_by_distance(&self, mut distances: Vec) -> Vec { let mut nodes_to_send = Vec::new(); distances.sort_unstable(); distances.dedup(); if let Some(0) = distances.first() { // if the distance is 0 send our local ENR nodes_to_send.push(self.local_enr.read().clone()); distances.remove(0); } if !distances.is_empty() { let mut kbuckets = self.kbuckets.write(); for node in kbuckets .nodes_by_distances(distances.as_slice(), self.config.max_nodes_response) .into_iter() .map(|entry| entry.node.value.clone()) { nodes_to_send.push(node); } } nodes_to_send } /// Mark a node in the routing table as `Disconnnected`. /// /// A `Disconnected` node will be present in the routing table and will be only /// used if there are no other `Connected` peers in the bucket. /// Returns `true` if node was in table and `false` otherwise. pub fn disconnect_node(&self, node_id: &NodeId) -> bool { let key = &kbucket::Key::from(*node_id); !matches!( self.kbuckets .write() .update_node_status(key, ConnectionState::Disconnected, None), UpdateResult::Failed(_) ) } /// Returns the number of connected peers that exist in the routing table. pub fn connected_peers(&self) -> usize { self.kbuckets .write() .iter() .filter(|entry| entry.status.is_connected()) .count() } /// Gets the metrics associated with the Server pub fn metrics(&self) -> Metrics { Metrics::from(&METRICS) } /// Exposes the raw reference to the underlying internal metrics. pub fn raw_metrics() -> &'static METRICS { &METRICS } /// Returns the local ENR of the node. pub fn local_enr(&self) -> Enr { self.local_enr.read().clone() } /// Returns an ENR if one is known for the given NodeId. pub fn find_enr(&self, node_id: &NodeId) -> Option { // check if we know this node id in our routing table let key = kbucket::Key::from(*node_id); if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { return Some(entry.value().clone()); } None } /// Bans a node from the server. This will remove the node from the routing table if it exists /// and block all incoming packets from the node until the timeout specified. Setting the /// timeout to `None` creates a permanent ban. pub fn ban_node(&self, node_id: &NodeId, duration_of_ban: Option) { let time_to_unban = duration_of_ban.map(|v| Instant::now() + v); self.remove_node(node_id); PERMIT_BAN_LIST .write() .ban_nodes .insert(*node_id, time_to_unban); } /// Removes a banned node from the banned list. pub fn ban_node_remove(&self, node_id: &NodeId) { PERMIT_BAN_LIST.write().ban_nodes.remove(node_id); } /// Permits a node, allowing the node to bypass the packet filter. pub fn permit_node(&self, node_id: &NodeId) { PERMIT_BAN_LIST.write().permit_nodes.insert(*node_id); } /// Removes a node from the permit list. pub fn permit_node_remove(&self, node_id: &NodeId) { PERMIT_BAN_LIST.write().permit_nodes.remove(node_id); } /// Bans an IP from the server. This will block all incoming packets from the IP. pub fn ban_ip(&self, ip: std::net::IpAddr, duration_of_ban: Option) { let time_to_unban = duration_of_ban.map(|v| Instant::now() + v); PERMIT_BAN_LIST.write().ban_ips.insert(ip, time_to_unban); } /// Removes a banned IP from the banned list. pub fn ban_ip_remove(&self, ip: &std::net::IpAddr) { PERMIT_BAN_LIST.write().ban_ips.remove(ip); } /// Permits an IP, allowing the all packets from the IP to bypass the packet filter. pub fn permit_ip(&self, ip: std::net::IpAddr) { PERMIT_BAN_LIST.write().permit_ips.insert(ip); } /// Removes an IP from the permit list. pub fn permit_ip_remove(&self, ip: &std::net::IpAddr) { PERMIT_BAN_LIST.write().permit_ips.remove(ip); } /// Updates the local ENR TCP/UDP socket. pub fn update_local_enr_socket(&self, socket_addr: SocketAddr, is_tcp: bool) -> bool { let local_socket = self.local_enr.read().udp_socket(); if local_socket != Some(socket_addr) { if is_tcp { self.local_enr .write() .set_tcp_socket(socket_addr, &self.enr_key.read()) .is_ok() } else { self.local_enr .write() .set_udp_socket(socket_addr, &self.enr_key.read()) .is_ok() } } else { false } } /// Allows application layer to insert an arbitrary field into the local ENR. pub fn enr_insert(&self, key: &str, value: &[u8]) -> Result>, EnrError> { self.local_enr .write() .insert(key, value, &self.enr_key.read()) .map(|v| v.map(|v| v.to_vec())) } /// Returns an iterator over all ENR node IDs of nodes currently contained in the routing table. pub fn table_entries_id(&self) -> Vec { self.kbuckets .write() .iter() .map(|entry| *entry.node.key.preimage()) .collect() } /// Returns an iterator over all the ENR's of nodes currently contained in the routing table. pub fn table_entries_enr(&self) -> Vec { self.kbuckets .write() .iter() .map(|entry| entry.node.value.clone()) .collect() } /// Returns an iterator over all the entries in the routing table. pub fn table_entries(&self) -> Vec<(NodeId, Enr, NodeStatus)> { self.kbuckets .write() .iter() .map(|entry| { ( *entry.node.key.preimage(), entry.node.value.clone(), entry.status, ) }) .collect() } /// Requests the ENR of a node corresponding to multiaddr or multi-addr string. /// /// Only `ed25519` and `secp256k1` key types are currently supported. /// /// Note: The async syntax is forgone here in order to create `'static` futures, where the /// underlying sending channel is cloned. #[cfg(feature = "libp2p")] #[cfg_attr(docsrs, doc(cfg(feature = "libp2p")))] pub fn request_enr( &self, multiaddr: impl std::convert::TryInto + 'static, ) -> impl Future> + 'static { let channel = self.clone_channel(); async move { let channel = channel.map_err(|_| RequestError::ServiceNotStarted)?; // Sanitize the multiaddr // The multiaddr must support the udp protocol and be of an appropriate key type. // The conversion logic is contained in the `TryFrom` implementation of a // `NodeContact`. let multiaddr: Multiaddr = multiaddr.try_into().map_err(|_| { RequestError::InvalidMultiaddr("Could not convert to multiaddr".into()) })?; let node_contact: NodeContact = NodeContact::try_from(multiaddr) .map_err(|e| RequestError::InvalidMultiaddr(e.into()))?; let (callback_send, callback_recv) = oneshot::channel(); let event = ServiceRequest::FindEnr(node_contact, callback_send); channel .send(event) .await .map_err(|_| RequestError::ChannelFailed("Service channel closed".into()))?; callback_recv .await .map_err(|e| RequestError::ChannelFailed(e.to_string()))? } } /// Request a TALK message from a node, identified via the ENR. pub fn talk_req( &self, enr: Enr, protocol: Vec, request: Vec, ) -> impl Future, RequestError>> + 'static { // convert the ENR to a node_contact. let node_contact = NodeContact::from(enr); // the service will verify if this node is contactable, we just send it and // await a response. let (callback_send, callback_recv) = oneshot::channel(); let channel = self.clone_channel(); async move { let channel = channel.map_err(|_| RequestError::ServiceNotStarted)?; let event = ServiceRequest::Talk(node_contact, protocol, request, callback_send); // send the request channel .send(event) .await .map_err(|_| RequestError::ChannelFailed("Service channel closed".into()))?; // await the response callback_recv .await .map_err(|e| RequestError::ChannelFailed(e.to_string()))? } } /// Runs an iterative `FIND_NODE` request. /// /// This will return peers containing contactable nodes of the DHT closest to the /// requested `NodeId`. /// /// Note: The async syntax is forgone here in order to create `'static` futures, where the /// underlying sending channel is cloned. pub fn find_node( &self, target_node: NodeId, ) -> impl Future, QueryError>> + 'static { let channel = self.clone_channel(); async move { let channel = channel.map_err(|_| QueryError::ServiceNotStarted)?; let (callback_send, callback_recv) = oneshot::channel(); let query_kind = QueryKind::FindNode { target_node }; let event = ServiceRequest::StartQuery(query_kind, callback_send); channel .send(event) .await .map_err(|_| QueryError::ChannelFailed("Service channel closed".into()))?; callback_recv .await .map_err(|e| QueryError::ChannelFailed(e.to_string())) } } /// Starts a `FIND_NODE` request. /// /// This will return less than or equal to `num_nodes` ENRs which satisfy the /// `predicate`. /// /// The predicate is a boxed function that takes an ENR reference and returns a boolean /// indicating if the record is applicable to the query or not. /// /// Note: The async syntax is forgone here in order to create `'static` futures, where the /// underlying sending channel is cloned. /// /// ### Example /// ```ignore /// let predicate = Box::new(|enr: &Enr| enr.ip().is_some()); /// let target = NodeId::random(); /// let result = discv5.find_node_predicate(target, predicate, 5).await; /// ``` pub fn find_node_predicate( &self, target_node: NodeId, predicate: Box bool + Send>, target_peer_no: usize, ) -> impl Future, QueryError>> + 'static { let channel = self.clone_channel(); async move { let channel = channel.map_err(|_| QueryError::ServiceNotStarted)?; let (callback_send, callback_recv) = oneshot::channel(); let query_kind = QueryKind::Predicate { target_node, predicate, target_peer_no, }; let event = ServiceRequest::StartQuery(query_kind, callback_send); channel .send(event) .await .map_err(|_| QueryError::ChannelFailed("Service channel closed".into()))?; callback_recv .await .map_err(|e| QueryError::ChannelFailed(e.to_string())) } } /// Creates an event stream channel which can be polled to receive Discv5 events. pub fn event_stream( &self, ) -> impl Future, Discv5Error>> + 'static { let channel = self.clone_channel(); async move { let channel = channel?; let (callback_send, callback_recv) = oneshot::channel(); let event = ServiceRequest::RequestEventStream(callback_send); channel .send(event) .await .map_err(|_| Discv5Error::ServiceChannelClosed)?; callback_recv .await .map_err(|_| Discv5Error::ServiceChannelClosed) } } /// Internal helper function to send events to the Service. fn clone_channel(&self) -> Result, Discv5Error> { if let Some(channel) = self.service_channel.as_ref() { Ok(channel.clone()) } else { Err(Discv5Error::ServiceNotStarted) } } } impl Drop for Discv5 { fn drop(&mut self) { self.shutdown(); } }