// Copyright 2015-2020 Parity Technologies (UK) Ltd. // This file is part of OpenEthereum. // OpenEthereum is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // OpenEthereum 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 General Public License for more details. // You should have received a copy of the GNU General Public License // along with OpenEthereum. If not, see . use discovery::{NodeEntry, TableUpdates}; use ethereum_types::H512; use ip_utils::*; use network::{AllowIP, Error, ErrorKind, IpFilter}; use rand::{self, Rng}; use rlp::{DecoderError, Rlp, RlpStream}; use serde_json; use std::{ collections::{HashMap, HashSet}, fmt::{self, Display, Formatter}, fs, hash::{Hash, Hasher}, net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}, path::PathBuf, slice, str::FromStr, time::{self, Duration, SystemTime}, }; /// Node public key pub type NodeId = H512; #[derive(Debug, Clone, PartialEq)] /// Node address info pub struct NodeEndpoint { /// IP(V4 or V6) address pub address: SocketAddr, /// Connection port. pub udp_port: u16, } impl NodeEndpoint { pub fn udp_address(&self) -> SocketAddr { match self.address { SocketAddr::V4(a) => SocketAddr::V4(SocketAddrV4::new(*a.ip(), self.udp_port)), SocketAddr::V6(a) => SocketAddr::V6(SocketAddrV6::new( *a.ip(), self.udp_port, a.flowinfo(), a.scope_id(), )), } } pub fn is_allowed(&self, filter: &IpFilter) -> bool { (self.is_allowed_by_predefined(&filter.predefined) || filter .custom_allow .iter() .any(|ipnet| self.address.ip().is_within(ipnet))) && !filter .custom_block .iter() .any(|ipnet| self.address.ip().is_within(ipnet)) } pub fn is_allowed_by_predefined(&self, filter: &AllowIP) -> bool { match filter { AllowIP::All => true, AllowIP::Private => self.address.ip().is_usable_private(), AllowIP::Public => self.address.ip().is_usable_public(), AllowIP::None => false, } } pub fn from_rlp(rlp: &Rlp) -> Result { let tcp_port = rlp.val_at::(2)?; let udp_port = rlp.val_at::(1)?; let addr_bytes = rlp.at(0)?.data()?; let address = match addr_bytes.len() { 4 => Ok(SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::new(addr_bytes[0], addr_bytes[1], addr_bytes[2], addr_bytes[3]), tcp_port, ))), 16 => unsafe { let o: *const u16 = addr_bytes.as_ptr() as *const u16; let o = slice::from_raw_parts(o, 8); Ok(SocketAddr::V6(SocketAddrV6::new( Ipv6Addr::new(o[0], o[1], o[2], o[3], o[4], o[5], o[6], o[7]), tcp_port, 0, 0, ))) }, _ => Err(DecoderError::RlpInconsistentLengthAndData), }?; Ok(NodeEndpoint { address, udp_port }) } pub fn to_rlp(&self, rlp: &mut RlpStream) { match self.address { SocketAddr::V4(a) => { rlp.append(&(&a.ip().octets()[..])); } SocketAddr::V6(a) => unsafe { let o: *const u8 = a.ip().segments().as_ptr() as *const u8; rlp.append(&slice::from_raw_parts(o, 16)); }, }; rlp.append(&self.udp_port); rlp.append(&self.address.port()); } pub fn to_rlp_list(&self, rlp: &mut RlpStream) { rlp.begin_list(3); self.to_rlp(rlp); } /// Validates that the tcp port is not 0 and that the node is a valid discovery node (i.e. `is_valid_discovery_node()` is true). /// Sync happens over tcp. pub fn is_valid_sync_node(&self) -> bool { self.is_valid_discovery_node() && self.address.port() != 0 } /// Validates that the udp port is not 0 and address IP is specified. /// Peer discovery happens over udp. pub fn is_valid_discovery_node(&self) -> bool { self.udp_port != 0 && match self.address { SocketAddr::V4(a) => !a.ip().is_unspecified(), SocketAddr::V6(a) => !a.ip().is_unspecified(), } } } impl FromStr for NodeEndpoint { type Err = Error; /// Create endpoint from string. Performs name resolution if given a host name. fn from_str(s: &str) -> Result { let address = s.to_socket_addrs().map(|mut i| i.next()); match address { Ok(Some(a)) => Ok(NodeEndpoint { address: a, udp_port: a.port(), }), Ok(None) => bail!(ErrorKind::AddressResolve(None)), Err(_) => Err(ErrorKind::AddressParse.into()), // always an io::Error of InvalidInput kind } } } #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum PeerType { _Required, Optional, } /// A type for representing an interaction (contact) with a node at a given time /// that was either a success or a failure. #[derive(Clone, Copy, Debug)] pub enum NodeContact { Success(SystemTime), Failure(SystemTime), } impl NodeContact { fn success() -> NodeContact { NodeContact::Success(SystemTime::now()) } fn failure() -> NodeContact { NodeContact::Failure(SystemTime::now()) } fn time(&self) -> SystemTime { match *self { NodeContact::Success(t) | NodeContact::Failure(t) => t, } } /// Filters and old contact, returning `None` if it happened longer than a /// week ago. fn recent(&self) -> Option<&NodeContact> { let t = self.time(); if let Ok(d) = t.elapsed() { if d < Duration::from_secs(60 * 60 * 24 * 7) { return Some(self); } } None } } #[derive(Debug)] pub struct Node { pub id: NodeId, pub endpoint: NodeEndpoint, pub peer_type: PeerType, pub last_contact: Option, } impl Node { pub fn new(id: NodeId, endpoint: NodeEndpoint) -> Node { Node { id, endpoint, peer_type: PeerType::Optional, last_contact: None, } } } impl Display for Node { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if self.endpoint.udp_port != self.endpoint.address.port() { write!( f, "enode://{:x}@{}+{}", self.id, self.endpoint.address, self.endpoint.udp_port )?; } else { write!(f, "enode://{:x}@{}", self.id, self.endpoint.address)?; } Ok(()) } } impl FromStr for Node { type Err = Error; fn from_str(s: &str) -> Result { let (id, endpoint) = if s.len() > 136 && &s[0..8] == "enode://" && &s[136..137] == "@" { ( s[8..136].parse().map_err(|_| ErrorKind::InvalidNodeId)?, NodeEndpoint::from_str(&s[137..])?, ) } else { (NodeId::new(), NodeEndpoint::from_str(s)?) }; Ok(Node { id, endpoint, peer_type: PeerType::Optional, last_contact: None, }) } } impl PartialEq for Node { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for Node {} impl Hash for Node { fn hash(&self, state: &mut H) where H: Hasher, { self.id.hash(state) } } const MAX_NODES: usize = 1024; const NODES_FILE: &str = "nodes.json"; /// Node table backed by disk file. pub struct NodeTable { nodes: HashMap, useless_nodes: HashSet, path: Option, } impl NodeTable { pub fn new(path: Option) -> NodeTable { NodeTable { path: path.clone(), nodes: NodeTable::load(path), useless_nodes: HashSet::new(), } } /// Add a node to table pub fn add_node(&mut self, mut node: Node) { // preserve node last_contact node.last_contact = self.nodes.get(&node.id).and_then(|n| n.last_contact); self.nodes.insert(node.id, node); } /// Returns a list of ordered nodes according to their most recent contact /// and filtering useless nodes. The algorithm for creating the sorted nodes /// is: /// - Contacts that aren't recent (older than 1 week) are discarded /// - (1) Nodes with a successful contact are ordered (most recent success first) /// - (2) Nodes with unknown contact (older than 1 week or new nodes) are randomly shuffled /// - (3) Nodes with a failed contact are ordered (oldest failure first) /// - The final result is the concatenation of (1), (2) and (3) fn ordered_entries(&self) -> Vec<&Node> { let mut success = Vec::new(); let mut failures = Vec::new(); let mut unknown = Vec::new(); let nodes = self .nodes .values() .filter(|n| !self.useless_nodes.contains(&n.id)); for node in nodes { // discard contact points older that aren't recent match node.last_contact.as_ref().and_then(|c| c.recent()) { Some(&NodeContact::Success(_)) => { success.push(node); } Some(&NodeContact::Failure(_)) => { failures.push(node); } None => { unknown.push(node); } } } success.sort_by(|a, b| { let a = a .last_contact .expect("vector only contains values with defined last_contact; qed"); let b = b .last_contact .expect("vector only contains values with defined last_contact; qed"); // inverse ordering, most recent successes come first b.time().cmp(&a.time()) }); failures.sort_by(|a, b| { let a = a .last_contact .expect("vector only contains values with defined last_contact; qed"); let b = b .last_contact .expect("vector only contains values with defined last_contact; qed"); // normal ordering, most distant failures come first a.time().cmp(&b.time()) }); rand::thread_rng().shuffle(&mut unknown); success.append(&mut unknown); success.append(&mut failures); success } /// Returns node ids sorted by failure percentage, for nodes with the same failure percentage the absolute number of /// failures is considered. pub fn nodes(&self, filter: &IpFilter) -> Vec { self.ordered_entries() .iter() .filter(|n| n.endpoint.is_allowed(&filter)) .map(|n| n.id) .collect() } /// Ordered list of all entries by failure percentage, for nodes with the same failure percentage the absolute /// number of failures is considered. pub fn entries(&self) -> Vec { self.ordered_entries() .iter() .map(|n| NodeEntry { endpoint: n.endpoint.clone(), id: n.id, }) .collect() } /// Get particular node pub fn get_mut(&mut self, id: &NodeId) -> Option<&mut Node> { self.nodes.get_mut(id) } /// Check if a node exists in the table. pub fn contains(&self, id: &NodeId) -> bool { self.nodes.contains_key(id) } /// Apply table changes coming from discovery pub fn update(&mut self, mut update: TableUpdates, reserved: &HashSet) { for (_, node) in update.added.drain() { let entry = self .nodes .entry(node.id) .or_insert_with(|| Node::new(node.id, node.endpoint.clone())); entry.endpoint = node.endpoint; } for r in update.removed { if !reserved.contains(&r) { self.nodes.remove(&r); } } } /// Set last contact as failure for a node pub fn note_failure(&mut self, id: &NodeId) { if let Some(node) = self.nodes.get_mut(id) { node.last_contact = Some(NodeContact::failure()); } } /// Set last contact as success for a node pub fn note_success(&mut self, id: &NodeId) { if let Some(node) = self.nodes.get_mut(id) { node.last_contact = Some(NodeContact::success()); } } /// Mark as useless, no further attempts to connect until next call to `clear_useless`. pub fn mark_as_useless(&mut self, id: &NodeId) { self.useless_nodes.insert(id.clone()); } /// Attempt to connect to useless nodes again. pub fn clear_useless(&mut self) { self.useless_nodes.clear(); } /// Save the nodes.json file. pub fn save(&self) { let mut path = match self.path { Some(ref path) => PathBuf::from(path), None => return, }; if let Err(e) = fs::create_dir_all(&path) { warn!(target: "network", "Error creating node table directory: {:?}", e); return; } path.push(NODES_FILE); let node_ids = self.nodes(&IpFilter::default()); let nodes = node_ids .into_iter() .map(|id| { self.nodes .get(&id) .expect("self.nodes() only returns node IDs from self.nodes") }) .take(MAX_NODES) .map(Into::into) .collect(); let table = json::NodeTable { nodes }; match fs::File::create(&path) { Ok(file) => { if let Err(e) = serde_json::to_writer_pretty(file, &table) { warn!(target: "network", "Error writing node table file: {:?}", e); } } Err(e) => { warn!(target: "network", "Error creating node table file: {:?}", e); } } } fn load(path: Option) -> HashMap { let path = match path { Some(path) => PathBuf::from(path).join(NODES_FILE), None => return Default::default(), }; let file = match fs::File::open(&path) { Ok(file) => file, Err(e) => { debug!(target: "network", "Error opening node table file: {:?}", e); return Default::default(); } }; let res: Result = serde_json::from_reader(file); match res { Ok(table) => table .nodes .into_iter() .filter_map(|n| n.into_node()) .map(|n| (n.id, n)) .collect(), Err(e) => { warn!(target: "network", "Error reading node table file: {:?}", e); Default::default() } } } } impl Drop for NodeTable { fn drop(&mut self) { self.save(); } } /// Check if node url is valid pub fn validate_node_url(url: &str) -> Option { Node::from_str(url).err() } mod json { use super::*; #[derive(Serialize, Deserialize)] pub struct NodeTable { pub nodes: Vec, } #[derive(Serialize, Deserialize)] pub enum NodeContact { #[serde(rename = "success")] Success(u64), #[serde(rename = "failure")] Failure(u64), } impl NodeContact { pub fn into_node_contact(self) -> super::NodeContact { match self { NodeContact::Success(s) => { super::NodeContact::Success(time::UNIX_EPOCH + Duration::from_secs(s)) } NodeContact::Failure(s) => { super::NodeContact::Failure(time::UNIX_EPOCH + Duration::from_secs(s)) } } } } #[derive(Serialize, Deserialize)] pub struct Node { pub url: String, pub last_contact: Option, } impl Node { pub fn into_node(self) -> Option { match super::Node::from_str(&self.url) { Ok(mut node) => { node.last_contact = self.last_contact.map(|c| c.into_node_contact()); Some(node) } _ => None, } } } impl<'a> From<&'a super::Node> for Node { fn from(node: &'a super::Node) -> Self { let last_contact = node.last_contact.and_then(|c| match c { super::NodeContact::Success(t) => t .duration_since(time::UNIX_EPOCH) .ok() .map(|d| NodeContact::Success(d.as_secs())), super::NodeContact::Failure(t) => t .duration_since(time::UNIX_EPOCH) .ok() .map(|d| NodeContact::Failure(d.as_secs())), }); Node { url: format!("{}", node), last_contact, } } } } #[cfg(test)] mod tests { use super::*; use ethereum_types::H512; use ipnetwork::IpNetwork; use std::{ net::{Ipv4Addr, SocketAddr, SocketAddrV4}, str::FromStr, }; use tempdir::TempDir; #[test] fn endpoint_parse() { let endpoint = NodeEndpoint::from_str("123.99.55.44:7770"); assert!(endpoint.is_ok()); let v4 = match endpoint.unwrap().address { SocketAddr::V4(v4address) => v4address, _ => panic!("should be v4 address"), }; assert_eq!(SocketAddrV4::new(Ipv4Addr::new(123, 99, 55, 44), 7770), v4); } #[test] fn endpoint_parse_empty_ip_string_returns_error() { let endpoint = NodeEndpoint::from_str(""); assert!(endpoint.is_err()); assert_matches!(endpoint.unwrap_err().kind(), &ErrorKind::AddressParse); } #[test] fn endpoint_parse_invalid_ip_string_returns_error() { let endpoint = NodeEndpoint::from_str("beef"); assert!(endpoint.is_err()); assert_matches!(endpoint.unwrap_err().kind(), &ErrorKind::AddressParse); } #[test] fn endpoint_parse_valid_ip_without_port_returns_error() { let endpoint = NodeEndpoint::from_str("123.123.123.123"); assert!(endpoint.is_err()); assert_matches!(endpoint.unwrap_err().kind(), &ErrorKind::AddressParse); let endpoint = NodeEndpoint::from_str("123.123.123.123:123"); assert!(endpoint.is_ok()) } #[test] fn node_parse() { assert!(validate_node_url("enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").is_none()); let node = Node::from_str("enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770"); assert!(node.is_ok()); let node = node.unwrap(); let v4 = match node.endpoint.address { SocketAddr::V4(v4address) => v4address, _ => panic!("should ve v4 address"), }; assert_eq!(SocketAddrV4::new(Ipv4Addr::new(22, 99, 55, 44), 7770), v4); assert_eq!( H512::from_str("a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(), node.id); } #[test] fn node_parse_fails_for_invalid_urls() { let node = Node::from_str("foo"); assert!(node.is_err()); assert_matches!(node.unwrap_err().kind(), &ErrorKind::AddressParse); let node = Node::from_str("enode://foo@bar"); assert!(node.is_err()); assert_matches!(node.unwrap_err().kind(), &ErrorKind::AddressParse); } #[test] fn table_last_contact_order() { let node1 = Node::from_str("enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node2 = Node::from_str("enode://b979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node3 = Node::from_str("enode://c979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node4 = Node::from_str("enode://d979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node5 = Node::from_str("enode://e979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node6 = Node::from_str("enode://f979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let id1 = H512::from_str("a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id2 = H512::from_str("b979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id3 = H512::from_str("c979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id4 = H512::from_str("d979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id5 = H512::from_str("e979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id6 = H512::from_str("f979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let mut table = NodeTable::new(None); table.add_node(node1); table.add_node(node2); table.add_node(node3); table.add_node(node4); table.add_node(node5); table.add_node(node6); // failures - nodes 1 & 2 table.note_failure(&id1); table.note_failure(&id2); // success - nodes 3 & 4 table.note_success(&id3); table.note_success(&id4); // success - node 5 (old contact) table.get_mut(&id5).unwrap().last_contact = Some(NodeContact::Success(time::UNIX_EPOCH)); // unknown - node 6 // nodes are also ordered according to their addition time // // nanosecond precision lost since mac os x high sierra update so let's not compare their order // https://github.com/openethereum/openethereum/issues/9632 let r = table.nodes(&IpFilter::default()); // most recent success assert!((r[0] == id4 && r[1] == id3) || (r[0] == id3 && r[1] == id4)); // unknown (old contacts and new nodes), randomly shuffled assert!((r[2] == id5 && r[3] == id6) || (r[2] == id6 && r[3] == id5)); // oldest failure assert!((r[4] == id1 && r[5] == id2) || (r[4] == id2 && r[5] == id1)); } #[test] fn table_save_load() { let tempdir = TempDir::new("").unwrap(); let node1 = Node::from_str("enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node2 = Node::from_str("enode://b979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let node3 = Node::from_str("enode://c979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@22.99.55.44:7770").unwrap(); let id1 = H512::from_str("a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id2 = H512::from_str("b979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); let id3 = H512::from_str("c979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap(); { let mut table = NodeTable::new(Some(tempdir.path().to_str().unwrap().to_owned())); table.add_node(node1); table.add_node(node2); table.add_node(node3); table.note_success(&id2); table.note_failure(&id3); } { let table = NodeTable::new(Some(tempdir.path().to_str().unwrap().to_owned())); let r = table.nodes(&IpFilter::default()); assert_eq!(r[0][..], id2[..]); // latest success assert_eq!(r[1][..], id1[..]); // unknown assert_eq!(r[2][..], id3[..]); // oldest failure } } #[test] fn custom_allow() { let filter = IpFilter { predefined: AllowIP::None, custom_allow: vec![ IpNetwork::from_str(&"10.0.0.0/8").unwrap(), IpNetwork::from_str(&"1.0.0.0/8").unwrap(), ], custom_block: vec![], }; assert!(!NodeEndpoint::from_str("123.99.55.44:7770") .unwrap() .is_allowed(&filter)); assert!(NodeEndpoint::from_str("10.0.0.1:7770") .unwrap() .is_allowed(&filter)); assert!(NodeEndpoint::from_str("1.0.0.55:5550") .unwrap() .is_allowed(&filter)); } #[test] fn custom_block() { let filter = IpFilter { predefined: AllowIP::All, custom_allow: vec![], custom_block: vec![ IpNetwork::from_str(&"10.0.0.0/8").unwrap(), IpNetwork::from_str(&"1.0.0.0/8").unwrap(), ], }; assert!(NodeEndpoint::from_str("123.99.55.44:7770") .unwrap() .is_allowed(&filter)); assert!(!NodeEndpoint::from_str("10.0.0.1:7770") .unwrap() .is_allowed(&filter)); assert!(!NodeEndpoint::from_str("1.0.0.55:5550") .unwrap() .is_allowed(&filter)); } #[test] fn custom_allow_ipv6() { let filter = IpFilter { predefined: AllowIP::None, custom_allow: vec![IpNetwork::from_str(&"fc00::/8").unwrap()], custom_block: vec![], }; assert!(NodeEndpoint::from_str("[fc00::]:5550") .unwrap() .is_allowed(&filter)); assert!(!NodeEndpoint::from_str("[fd00::]:5550") .unwrap() .is_allowed(&filter)); } #[test] fn custom_block_ipv6() { let filter = IpFilter { predefined: AllowIP::All, custom_allow: vec![], custom_block: vec![IpNetwork::from_str(&"fc00::/8").unwrap()], }; assert!(!NodeEndpoint::from_str("[fc00::]:5550") .unwrap() .is_allowed(&filter)); assert!(NodeEndpoint::from_str("[fd00::]:5550") .unwrap() .is_allowed(&filter)); } }