From 392653c659335a07dfb47ae8959e0c7d66182244 Mon Sep 17 00:00:00 2001 From: SyntheticBird Date: Thu, 5 Jun 2025 19:54:19 +0000 Subject: [PATCH] Define Tor Zone, add onion addressing and more (#481) * Define Tor Zone, add onion addressing, extend AddressBook, adapt `handle_timed_sync_request`, changes in cuprated + some cleanup In `cuprated`: - Added `Z: NetworkZone` as a generic requirement for `address_book_config`. Now takes the optional node address in argument. - Added `tor_net_seed_nodes` fn for obtaining tor seed nodes. - Added `CrossNetworkInternalPeerId::Tor(_)` variant and `From>` In `cuprate-wire`: - Added `src/network_address/onion_addr.rs` implementing `OnionAddr` type used by `Tor` network zone. - Implemented parsing, formatting, conversion and validation of `OnionAddr`. - Implemented 2 validation tests and 2 parsing tests for `OnionAddr`. - Documented and cleaned up `src/network_address/epee_builder.rs`. - Changed `u8` `type` field of `TaggedNetworkAddress` to `AddressType` enum equivalent to monerod's `address_type`. - Added additional `host` and `port` fields to `AllFieldedNetworkAddress` collection type. - Added `NetworkAddress:Tor` variant and added conversion to related functions. In `cuprate-address-book`: - Added `our_own_addr: Z::Addr` field to AddressBookConfig. This adds a `Z: NetworkZone` requirement to `AddressBookConfig`. - Adapted code to the new generic requirement. - Implemented handling of new `AddressBookRequest::OwnAddress` for querying the node self specified address for the zone. In `cuprate-p2p`: - If `Z::BROADCAST_OUR_OWN_ADDR` = `true`, `handle_timed_sync_request` will insert the node's address to the peerlist being sent. In `cuprate-p2p-core`: - Removed `#[async_trait::async_trait]` attribute to `impl NetworkZone for *`. - Added `AddressBookRequest::OwnAddress` and `AddressBookResponse::OwnAddress(Option)`. - Defined new `Tor` `NetworkZone` * fmt * fix typo and fmt * final edits? * fix test * forgor --- Cargo.lock | 2 + binaries/cuprated/src/config.rs | 10 +- binaries/cuprated/src/config/p2p.rs | 37 ++- binaries/cuprated/src/p2p/network_address.rs | 15 +- net/wire/Cargo.toml | 9 +- net/wire/src/lib.rs | 2 +- net/wire/src/network_address.rs | 8 +- net/wire/src/network_address/epee_builder.rs | 62 ++++- net/wire/src/network_address/onion_addr.rs | 233 ++++++++++++++++++ p2p/address-book/src/book.rs | 7 +- p2p/address-book/src/book/tests.rs | 5 +- p2p/address-book/src/lib.rs | 7 +- p2p/address-book/src/store.rs | 4 +- p2p/p2p-core/Cargo.toml | 1 + .../src/client/handshaker/builder/dummy.rs | 1 + p2p/p2p-core/src/client/request_handler.rs | 64 ++++- p2p/p2p-core/src/lib.rs | 2 +- p2p/p2p-core/src/network_zones.rs | 2 + p2p/p2p-core/src/network_zones/clear.rs | 1 - p2p/p2p-core/src/network_zones/tor.rs | 52 ++++ p2p/p2p-core/src/services.rs | 9 + p2p/p2p/src/config.rs | 2 +- test-utils/src/test_netzone.rs | 3 +- 23 files changed, 490 insertions(+), 48 deletions(-) create mode 100644 net/wire/src/network_address/onion_addr.rs create mode 100644 p2p/p2p-core/src/network_zones/tor.rs diff --git a/Cargo.lock b/Cargo.lock index fd89ca7..352b4a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -951,6 +951,7 @@ dependencies = [ "futures", "hex", "hex-literal", + "rand", "thiserror", "tokio", "tokio-stream", @@ -1095,6 +1096,7 @@ dependencies = [ "cuprate-levin", "cuprate-types", "hex", + "proptest", "thiserror", ] diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs index bbd8c77..d74deeb 100644 --- a/binaries/cuprated/src/config.rs +++ b/binaries/cuprated/src/config.rs @@ -219,11 +219,11 @@ impl Config { gray_peers_percent: self.p2p.clear_net.gray_peers_percent, p2p_port: self.p2p.clear_net.p2p_port, rpc_port: self.rpc.restricted.port_for_p2p(), - address_book_config: self - .p2p - .clear_net - .address_book_config - .address_book_config(&self.fs.cache_directory, self.network), + address_book_config: self.p2p.clear_net.address_book_config.address_book_config( + &self.fs.cache_directory, + self.network, + None, + ), } } diff --git a/binaries/cuprated/src/config/p2p.rs b/binaries/cuprated/src/config/p2p.rs index d5b521d..758b9e9 100644 --- a/binaries/cuprated/src/config/p2p.rs +++ b/binaries/cuprated/src/config/p2p.rs @@ -1,4 +1,5 @@ use std::{ + marker::PhantomData, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::Path, time::Duration, @@ -10,8 +11,9 @@ use cuprate_helper::{fs::address_book_path, network::Network}; use cuprate_p2p::config::TransportConfig; use cuprate_p2p_core::{ transports::{Tcp, TcpServerConfig}, - ClearNet, Transport, + ClearNet, NetworkZone, Transport, }; +use cuprate_wire::OnionAddr; use super::macros::config_struct; @@ -266,16 +268,23 @@ impl Default for AddressBookConfig { impl AddressBookConfig { /// Returns the [`cuprate_address_book::AddressBookConfig`]. - pub fn address_book_config( + pub fn address_book_config( &self, cache_dir: &Path, network: Network, - ) -> cuprate_address_book::AddressBookConfig { + our_own_address: Option, + ) -> cuprate_address_book::AddressBookConfig { + assert!( + !Z::BROADCAST_OWN_ADDR && our_own_address.is_some(), + "This network DO NOT take an incoming address." + ); + cuprate_address_book::AddressBookConfig { max_white_list_length: self.max_white_list_length, max_gray_list_length: self.max_gray_list_length, peer_store_directory: address_book_path(cache_dir, network), peer_save_period: self.peer_save_period, + our_own_address, } } } @@ -317,3 +326,25 @@ pub fn clear_net_seed_nodes(network: Network) -> Vec { .collect::>() .unwrap() } + +/// Seed nodes for `Tor`. +pub fn tor_net_seed_nodes(network: Network) -> Vec { + let seeds = match network { + Network::Mainnet => [ + "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083", + "qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion:18083", + "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18083", + "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18083", + "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18083", + "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18083", + ] + .as_slice(), + Network::Stagenet | Network::Testnet => [].as_slice(), + }; + + seeds + .iter() + .map(|s| s.parse()) + .collect::>() + .unwrap() +} diff --git a/binaries/cuprated/src/p2p/network_address.rs b/binaries/cuprated/src/p2p/network_address.rs index 7fa8e86..a342316 100644 --- a/binaries/cuprated/src/p2p/network_address.rs +++ b/binaries/cuprated/src/p2p/network_address.rs @@ -1,16 +1,25 @@ use std::net::SocketAddr; -use cuprate_p2p_core::{client::InternalPeerID, ClearNet, NetworkZone}; +use cuprate_p2p_core::{client::InternalPeerID, ClearNet, NetworkZone, Tor}; +use cuprate_wire::OnionAddr; /// An identifier for a P2P peer on any network. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CrossNetworkInternalPeerId { /// A clear-net peer. ClearNet(InternalPeerID<::Addr>), + /// A Tor onion peer. + Tor(InternalPeerID<::Addr>), } -impl From::Addr>> for CrossNetworkInternalPeerId { - fn from(addr: InternalPeerID<::Addr>) -> Self { +impl From> for CrossNetworkInternalPeerId { + fn from(addr: InternalPeerID) -> Self { Self::ClearNet(addr) } } + +impl From> for CrossNetworkInternalPeerId { + fn from(addr: InternalPeerID) -> Self { + Self::Tor(addr) + } +} diff --git a/net/wire/Cargo.toml b/net/wire/Cargo.toml index 3438091..84f413c 100644 --- a/net/wire/Cargo.toml +++ b/net/wire/Cargo.toml @@ -17,14 +17,15 @@ cuprate-fixed-bytes = { workspace = true } cuprate-types = { workspace = true, default-features = false, features = ["epee"] } cuprate-helper = { workspace = true, default-features = false, features = ["map"] } -bitflags = { workspace = true, features = ["std"] } -bytes = { workspace = true, features = ["std"] } -thiserror = { workspace = true } +bitflags = { workspace = true, features = ["std"] } +bytes = { workspace = true, features = ["std"] } +thiserror = { workspace = true } arbitrary = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] -hex = { workspace = true, features = ["std"]} +hex = { workspace = true, features = ["std"]} +proptest = { workspace = true } [lints] workspace = true diff --git a/net/wire/src/lib.rs b/net/wire/src/lib.rs index 674a2e9..cc33905 100644 --- a/net/wire/src/lib.rs +++ b/net/wire/src/lib.rs @@ -26,7 +26,7 @@ pub mod network_address; pub mod p2p; pub use cuprate_levin::BucketError; -pub use network_address::{NetZone, NetworkAddress}; +pub use network_address::{NetZone, NetworkAddress, OnionAddr}; pub use p2p::*; // re-export. diff --git a/net/wire/src/network_address.rs b/net/wire/src/network_address.rs index 3e15c46..91c92f7 100644 --- a/net/wire/src/network_address.rs +++ b/net/wire/src/network_address.rs @@ -26,6 +26,9 @@ use cuprate_epee_encoding::EpeeObject; mod epee_builder; use epee_builder::*; +mod onion_addr; +pub use onion_addr::*; + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum NetZone { Public, @@ -38,6 +41,7 @@ pub enum NetZone { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum NetworkAddress { Clear(SocketAddr), + Tor(OnionAddr), } impl EpeeObject for NetworkAddress { @@ -56,6 +60,7 @@ impl NetworkAddress { pub const fn get_zone(&self) -> NetZone { match self { Self::Clear(_) => NetZone::Public, + Self::Tor(_) => NetZone::Tor, } } @@ -72,6 +77,7 @@ impl NetworkAddress { pub const fn port(&self) -> u16 { match self { Self::Clear(ip) => ip.port(), + Self::Tor(addr) => addr.port(), } } } @@ -106,7 +112,7 @@ impl TryFrom for SocketAddr { fn try_from(value: NetworkAddress) -> Result { match value { NetworkAddress::Clear(addr) => Ok(addr), - //_ => Err(NetworkAddressIncorrectZone) + NetworkAddress::Tor(_) => Err(NetworkAddressIncorrectZone), } } } diff --git a/net/wire/src/network_address/epee_builder.rs b/net/wire/src/network_address/epee_builder.rs index 8b14644..1c20649 100644 --- a/net/wire/src/network_address/epee_builder.rs +++ b/net/wire/src/network_address/epee_builder.rs @@ -1,21 +1,37 @@ +//! Address epee serialization +//! +//! Addresses needs to be serialized into a specific format before being sent to other peers. +//! This module is handling this particular construction. +//! + +//---------------------------------------------------------------------------------------------------- Imports + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use bytes::Buf; use thiserror::Error; use cuprate_epee_encoding::{epee_object, EpeeObjectBuilder}; +use cuprate_types::AddressType; use crate::NetworkAddress; +use super::OnionAddr; + +//---------------------------------------------------------------------------------------------------- Network address construction + #[derive(Default)] +/// A serialized network address being communicated to or from a peer. pub struct TaggedNetworkAddress { - ty: Option, + /// Type of the network address (used later for conversion) + ty: Option, + /// All possible fields for a network address addr: Option, } epee_object!( TaggedNetworkAddress, - ty("type"): Option, + ty("type"): Option, addr: Option, ); @@ -75,31 +91,57 @@ impl From for TaggedNetworkAddress { match value { NetworkAddress::Clear(addr) => match addr { SocketAddr::V4(addr) => Self { - ty: Some(1), + ty: Some(AddressType::Ipv4), addr: Some(AllFieldsNetworkAddress { m_ip: Some(u32::from_le_bytes(addr.ip().octets())), m_port: Some(addr.port()), addr: None, + host: None, + port: None, }), }, SocketAddr::V6(addr) => Self { - ty: Some(2), + ty: Some(AddressType::Ipv6), addr: Some(AllFieldsNetworkAddress { addr: Some(addr.ip().octets()), m_port: Some(addr.port()), m_ip: None, + host: None, + port: None, }), }, }, + NetworkAddress::Tor(onion_addr) => Self { + ty: Some(AddressType::Tor), + addr: Some(AllFieldsNetworkAddress { + m_ip: None, + m_port: None, + addr: None, + host: Some(onion_addr.addr_string()), + port: Some(onion_addr.port()), + }), + }, } } } #[derive(Default)] +/// There are no ordering guarantees in epee format and as such all potential fields can be collected during deserialization. +/// The [`AllFieldsNetworkAddress`] is containing, as its name suggest, all optional field describing an address , as if it +/// could be of any type. struct AllFieldsNetworkAddress { + /// IPv4 address m_ip: Option, + /// IP port field m_port: Option, + + /// IPv6 address addr: Option<[u8; 16]>, + + /// Alternative network domain name (.onion or .i2p) + host: Option, + /// Alternative network virtual port + port: Option, } epee_object!( @@ -107,21 +149,27 @@ epee_object!( m_ip: Option, m_port: Option, addr: Option<[u8; 16]>, + host: Option, + port: Option, ); impl AllFieldsNetworkAddress { - fn try_into_network_address(self, ty: u8) -> Option { + fn try_into_network_address(self, ty: AddressType) -> Option { Some(match ty { - 1 => NetworkAddress::from(SocketAddrV4::new( + AddressType::Ipv4 => NetworkAddress::from(SocketAddrV4::new( Ipv4Addr::from(self.m_ip?.to_le_bytes()), self.m_port?, )), - 2 => NetworkAddress::from(SocketAddrV6::new( + AddressType::Ipv6 => NetworkAddress::from(SocketAddrV6::new( Ipv6Addr::from(self.addr?), self.m_port?, 0, 0, )), + AddressType::Tor => { + NetworkAddress::from(OnionAddr::new(self.host?.as_str(), self.port?).ok()?) + } + // Invalid _ => return None, }) } diff --git a/net/wire/src/network_address/onion_addr.rs b/net/wire/src/network_address/onion_addr.rs new file mode 100644 index 0000000..ce4632d --- /dev/null +++ b/net/wire/src/network_address/onion_addr.rs @@ -0,0 +1,233 @@ +//! Onion address +//! +//! This module define v3 Tor onion addresses +//! + +use std::{ + fmt::Display, + str::{self, FromStr}, +}; + +use thiserror::Error; + +use super::{NetworkAddress, NetworkAddressIncorrectZone}; + +/// A v3, `Copy`able onion address. +#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)] +pub struct OnionAddr { + /// 56 characters encoded onion v3 domain without the .onion suffix + /// + domain: [u8; 56], + /// Virtual port of the peer + pub port: u16, +} + +/// Error enum at parsing onion addresses +#[derive(Debug, Error)] +pub enum OnionAddrParsingError { + #[error("Address is either too long or short, length: {0}")] + InvalidLength(usize), + #[error("Address contain non-utf8 code point at tld byte location: {0:x}")] + NonUtf8Char(u8), + #[error("This is not an onion address, Tld: {0}")] + InvalidTld(String), + #[error("Domain contains non base32 characters")] + NonBase32Char, + #[error("Invalid version. Found: {0}")] + InvalidVersion(u8), + #[error("The checksum is invalid.")] + InvalidChecksum, + #[error("Invalid port specified")] + InvalidPort, +} + +impl OnionAddr { + /// Attempt to create an [`OnionAddr`] from a complete .onion address string and a port. + /// + /// Return an [`OnionAddrParsingError`] if the supplied `addr` is invalid. + pub fn new(addr: &str, port: u16) -> Result { + Self::check_addr(addr).map(|d| Self { domain: d, port }) + } + + /// Establish if the .onion address is valid. + /// + /// Return the 56 character domain bytes if valid, `OnionAddrParsingError` otherwise. + pub fn check_addr(addr: &str) -> Result<[u8; 56], OnionAddrParsingError> { + // v3 onion addresses are 62 characters long + if addr.len() != 62 { + return Err(OnionAddrParsingError::InvalidLength(addr.len())); + } + + let Some((domain, tld)) = addr.split_at_checked(56) else { + return Err(OnionAddrParsingError::NonUtf8Char(addr.as_bytes()[56])); + }; + + // The ".onion" suffix must be located at the 57th byte. + if tld != ".onion" { + return Err(OnionAddrParsingError::InvalidTld(String::from(tld))); + } + + // The domain part must only contain base32 characters. + if !domain + .as_bytes() + .iter() + .copied() + .all(|c| c.is_ascii_lowercase() || (b'2'..=b'7').contains(&c)) + { + return Err(OnionAddrParsingError::NonBase32Char); + } + + Ok(addr.as_bytes()[..56] + .try_into() + .unwrap_or_else(|e| panic!("We just validated address: {addr} : {e}"))) + } + + /// Generate an onion address string. + /// + /// Returns a `String` containing the onion domain name and ".onion" TLD only, in form of `zbjkbs...ofptid.onion`. + pub fn addr_string(&self) -> String { + let mut domain = str::from_utf8(&self.domain) + .expect("Onion addresses are always containing UTF-8 characters.") + .to_string(); + + domain.push_str(".onion"); + domain + } + + #[inline] + pub const fn port(&self) -> u16 { + self.port + } + + #[inline] + pub const fn domain(&self) -> [u8; 56] { + self.domain + } +} + +/// Display for [`OnionAddr`]. It prints the onion address and port, in the form of `.onion:` +impl Display for OnionAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let domain = str::from_utf8(&self.domain) + .expect("Onion addresses are always containing UTF-8 characters."); + + f.write_str(domain)?; + f.write_str(".onion:")?; + self.port.fmt(f) + } +} + +/// [`OnionAddr`] parses an onion address **and a port**. +impl FromStr for OnionAddr { + type Err = OnionAddrParsingError; + + fn from_str(addr: &str) -> Result { + let (addr, port) = addr + .split_at_checked(62) + .ok_or(OnionAddrParsingError::InvalidLength(addr.len()))?; + + // Port + let port: u16 = port + .starts_with(':') + .then(|| port[1..].parse().ok()) + .flatten() + .ok_or(OnionAddrParsingError::InvalidPort)?; + + // Address + let domain = Self::check_addr(addr)?; + + Ok(Self { domain, port }) + } +} + +impl TryFrom for OnionAddr { + type Error = NetworkAddressIncorrectZone; + fn try_from(value: NetworkAddress) -> Result { + match value { + NetworkAddress::Tor(addr) => Ok(addr), + NetworkAddress::Clear(_) => Err(NetworkAddressIncorrectZone), + } + } +} + +impl From for NetworkAddress { + fn from(value: OnionAddr) -> Self { + Self::Tor(value) + } +} + +#[cfg(test)] +mod tests { + use proptest::{collection::vec, prelude::*}; + + use super::OnionAddr; + + const VALID_ONION_ADDRESSES: &[&str] = &[ + "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", // Tor Website + "pzhdfe7jraknpj2qgu5cz2u3i4deuyfwmonvzu5i3nyw4t4bmg7o5pad.onion", // Tor Blog + "monerotoruzizulg5ttgat2emf4d6fbmiea25detrmmy7erypseyteyd.onion", // Monero Website + "sfprivg7qec6tdle7u6hdepzjibin6fn3ivm6qlwytr235rh5vc6bfqd.onion", // SethForPrivacy + "yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion", // P2Pool + "p2pool2giz2r5cpqicajwoazjcxkfujxswtk3jolfk2ubilhrkqam2id.onion", // P2Pool Observer + "d6ac5qatnyodxisdehb3i4m7edfvtukxzhhtyadbgaxghcxee2xadpid.onion", // Rucknium ♥ + "duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion", // DuckDuckGo + "featherdvtpi7ckdbkb2yxjfwx3oyvr3xjz3oo4rszylfzjdg6pbm3id.onion", // Feather wallet + "revuo75joezkbeitqmas4ab6spbrkr4vzbhjmeuv75ovrfqfp47mtjid.onion", // Revuo + "xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion", // PrivacyGuides.org + "allyouhavetodecideiswhattodowiththetimethatisgiventoyouu.onion", // Gandalf the Grey + // Tor mainnet seed nodes as of 2025-05-15 with random ports + "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion", + "qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion", + "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", + "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion", + "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion", + "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion", + ]; + + #[test] + fn valid_onion_address() { + for addr in VALID_ONION_ADDRESSES { + assert!( + OnionAddr::check_addr(addr).is_ok(), + "Address {addr} has been reported as invalid." + ); + } + } + + proptest! { + #[test] + fn parse_valid_onion_address_w_port(ports in vec(any::(), 18)) { + for (addr,port) in VALID_ONION_ADDRESSES.iter().zip(ports) { + + let mut s = (*addr).to_string(); + s.push(':'); + s.push_str(&port.to_string()); + + assert!( + s.parse::().is_ok(), + "Address {addr} has been reported as invalid." + ); + } + } + + #[test] + fn invalid_onion_address(addresses in vec("[a-z][2-7]{56}.onion", 250)) { + for addr in addresses { + assert!( + OnionAddr::check_addr(&addr).is_err(), + "Address {addr} has been reported as valid." + ); + } + } + + #[test] + fn parse_invalid_onion_address_w_port(addresses in vec("[a-z][2-7]{56}.onion:[0-9]{1,5}", 250)) { + for addr in addresses { + assert!( + addr.parse::().is_err(), + "Address {addr} has been reported as valid." + ); + } + } + } +} diff --git a/p2p/address-book/src/book.rs b/p2p/address-book/src/book.rs index 5f33fc9..462be00 100644 --- a/p2p/address-book/src/book.rs +++ b/p2p/address-book/src/book.rs @@ -66,12 +66,12 @@ pub struct AddressBook { peer_save_task_handle: Option>>, peer_save_interval: Interval, - cfg: AddressBookConfig, + cfg: AddressBookConfig, } impl AddressBook { pub fn new( - cfg: AddressBookConfig, + cfg: AddressBookConfig, white_peers: Vec>, gray_peers: Vec>, anchor_peers: Vec, @@ -417,6 +417,9 @@ impl Service> for AddressBook { AddressBookRequest::GetBan(addr) => Ok(AddressBookResponse::GetBan { unban_instant: self.peer_unban_instant(&addr).map(Instant::into_std), }), + AddressBookRequest::OwnAddress => { + Ok(AddressBookResponse::OwnAddress(self.cfg.our_own_address)) + } AddressBookRequest::Peerlist | AddressBookRequest::PeerlistSize | AddressBookRequest::ConnectionCount diff --git a/p2p/address-book/src/book/tests.rs b/p2p/address-book/src/book/tests.rs index b2c4c49..050dcc2 100644 --- a/p2p/address-book/src/book/tests.rs +++ b/p2p/address-book/src/book/tests.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, time::Duration}; use futures::StreamExt; use tokio::time::interval; -use cuprate_p2p_core::handles::HandleBuilder; +use cuprate_p2p_core::{handles::HandleBuilder, NetworkZone}; use cuprate_pruning::PruningSeed; use super::{AddressBook, ConnectionPeerEntry, InternalPeerID}; @@ -11,12 +11,13 @@ use crate::{peer_list::tests::make_fake_peer_list, AddressBookConfig, AddressBoo use cuprate_test_utils::test_netzone::{TestNetZone, TestNetZoneAddr}; -fn test_cfg() -> AddressBookConfig { +fn test_cfg() -> AddressBookConfig { AddressBookConfig { max_white_list_length: 100, max_gray_list_length: 500, peer_store_directory: PathBuf::new(), peer_save_period: Duration::from_secs(60), + our_own_address: None, } } diff --git a/p2p/address-book/src/lib.rs b/p2p/address-book/src/lib.rs index 74501fe..3c70433 100644 --- a/p2p/address-book/src/lib.rs +++ b/p2p/address-book/src/lib.rs @@ -20,7 +20,7 @@ mod store; /// The address book config. #[derive(Debug, Clone)] -pub struct AddressBookConfig { +pub struct AddressBookConfig { /// The maximum number of white peers in the peer list. /// /// White peers are peers we have connected to before. @@ -33,6 +33,9 @@ pub struct AddressBookConfig { pub peer_store_directory: PathBuf, /// The amount of time between saving the address book to disk. pub peer_save_period: Duration, + + /// Our own address to advertise to peers. (Only set if `Z::BROADCAST_OWN_ADDR` = `true`) + pub our_own_address: Option, } /// Possible errors when dealing with the address book. @@ -61,7 +64,7 @@ pub enum AddressBookError { /// Initializes the P2P address book for a specific network zone. pub async fn init_address_book( - cfg: AddressBookConfig, + cfg: AddressBookConfig, ) -> Result, std::io::Error> { let (white_list, gray_list) = match store::read_peers_from_disk::(&cfg).await { Ok(res) => res, diff --git a/p2p/address-book/src/store.rs b/p2p/address-book/src/store.rs index 7682839..35354ee 100644 --- a/p2p/address-book/src/store.rs +++ b/p2p/address-book/src/store.rs @@ -27,7 +27,7 @@ struct DeserPeerDataV1 { } pub(crate) fn save_peers_to_disk( - cfg: &AddressBookConfig, + cfg: &AddressBookConfig, white_list: &PeerList, gray_list: &PeerList, ) -> JoinHandle> { @@ -51,7 +51,7 @@ pub(crate) fn save_peers_to_disk( } pub(crate) async fn read_peers_from_disk( - cfg: &AddressBookConfig, + cfg: &AddressBookConfig, ) -> Result< ( Vec>, diff --git a/p2p/p2p-core/Cargo.toml b/p2p/p2p-core/Cargo.toml index 4515e5b..dd14373 100644 --- a/p2p/p2p-core/Cargo.toml +++ b/p2p/p2p-core/Cargo.toml @@ -24,6 +24,7 @@ tower = { workspace = true, features = ["util", "tracing", "make"] } cfg-if = { workspace = true } thiserror = { workspace = true } +rand = { workspace = true, features = ["std", "std_rng"] } tracing = { workspace = true, features = ["std", "attributes"] } hex-literal = { workspace = true } diff --git a/p2p/p2p-core/src/client/handshaker/builder/dummy.rs b/p2p/p2p-core/src/client/handshaker/builder/dummy.rs index 404f7a2..2036833 100644 --- a/p2p/p2p-core/src/client/handshaker/builder/dummy.rs +++ b/p2p/p2p-core/src/client/handshaker/builder/dummy.rs @@ -108,6 +108,7 @@ impl Service> for DummyAddressBook { AddressBookRequest::GetBan(_) => AddressBookResponse::GetBan { unban_instant: None, }, + AddressBookRequest::OwnAddress => AddressBookResponse::OwnAddress(None), AddressBookRequest::Peerlist | AddressBookRequest::PeerlistSize | AddressBookRequest::ConnectionCount diff --git a/p2p/p2p-core/src/client/request_handler.rs b/p2p/p2p-core/src/client/request_handler.rs index c2f3b8e..c3aa947 100644 --- a/p2p/p2p-core/src/client/request_handler.rs +++ b/p2p/p2p-core/src/client/request_handler.rs @@ -1,6 +1,8 @@ use futures::TryFutureExt; +use rand::{thread_rng, Rng}; use tower::ServiceExt; +use cuprate_pruning::PruningSeed; use cuprate_wire::{ admin::{ PingResponse, SupportFlagsResponse, TimedSyncRequest, TimedSyncResponse, @@ -14,6 +16,7 @@ use crate::{ constants::MAX_PEERS_IN_PEER_LIST_MESSAGE, services::{ AddressBookRequest, AddressBookResponse, CoreSyncDataRequest, CoreSyncDataResponse, + ZoneSpecificPeerListEntryBase, }, AddressBook, CoreSyncSvc, NetworkZone, PeerRequest, PeerResponse, ProtocolRequestHandler, }; @@ -101,18 +104,7 @@ where *self.peer_info.core_sync_data.lock().unwrap() = req.payload_data; - let AddressBookResponse::Peers(peers) = self - .address_book_svc - .ready() - .await? - .call(AddressBookRequest::GetWhitePeers( - MAX_PEERS_IN_PEER_LIST_MESSAGE, - )) - .await? - else { - panic!("Address book sent incorrect response!"); - }; - + // Fetch core sync data. let CoreSyncDataResponse(core_sync_data) = self .our_sync_svc .ready() @@ -120,6 +112,54 @@ where .call(CoreSyncDataRequest) .await?; + // Attempt to fetch our own address if supported by this network zone. + let own_addr = if Z::BROADCAST_OWN_ADDR { + let AddressBookResponse::OwnAddress(own_addr) = self + .address_book_svc + .ready() + .await? + .call(AddressBookRequest::OwnAddress) + .await? + else { + panic!("Address book sent incorrect response!"); + }; + + own_addr + } else { + None + }; + + let mut peer_list_req_size = MAX_PEERS_IN_PEER_LIST_MESSAGE; + if own_addr.is_some() { + peer_list_req_size -= 1; + } + + // Fetch a peerlist to send + let AddressBookResponse::Peers(mut peers) = self + .address_book_svc + .ready() + .await? + .call(AddressBookRequest::GetWhitePeers(peer_list_req_size)) + .await? + else { + panic!("Address book sent incorrect response!"); + }; + + if let Some(own_addr) = own_addr { + // Append our address to the final peer list + peers.insert( + thread_rng().gen_range(0..=peers.len()), + ZoneSpecificPeerListEntryBase { + adr: own_addr, + id: self.our_basic_node_data.peer_id, + last_seen: 0, + pruning_seed: PruningSeed::NotPruned, + rpc_port: self.our_basic_node_data.rpc_port, + rpc_credits_per_hash: self.our_basic_node_data.rpc_credits_per_hash, + }, + ); + } + Ok(TimedSyncResponse { payload_data: core_sync_data, local_peerlist_new: peers.into_iter().map(Into::into).collect(), diff --git a/p2p/p2p-core/src/lib.rs b/p2p/p2p-core/src/lib.rs index 15498af..979e5e2 100644 --- a/p2p/p2p-core/src/lib.rs +++ b/p2p/p2p-core/src/lib.rs @@ -87,7 +87,7 @@ pub mod transports; pub mod types; pub use error::*; -pub use network_zones::ClearNet; +pub use network_zones::{ClearNet, Tor}; pub use protocol::*; use services::*; //re-export diff --git a/p2p/p2p-core/src/network_zones.rs b/p2p/p2p-core/src/network_zones.rs index 7c83645..c2b4aeb 100644 --- a/p2p/p2p-core/src/network_zones.rs +++ b/p2p/p2p-core/src/network_zones.rs @@ -1,3 +1,5 @@ mod clear; +mod tor; pub use clear::ClearNet; +pub use tor::Tor; diff --git a/p2p/p2p-core/src/network_zones/clear.rs b/p2p/p2p-core/src/network_zones/clear.rs index 59e0132..0ff9383 100644 --- a/p2p/p2p-core/src/network_zones/clear.rs +++ b/p2p/p2p-core/src/network_zones/clear.rs @@ -27,7 +27,6 @@ impl NetZoneAddress for SocketAddr { #[derive(Clone, Copy)] pub enum ClearNet {} -#[async_trait::async_trait] impl NetworkZone for ClearNet { const NAME: &'static str = "ClearNet"; diff --git a/p2p/p2p-core/src/network_zones/tor.rs b/p2p/p2p-core/src/network_zones/tor.rs new file mode 100644 index 0000000..25baa3e --- /dev/null +++ b/p2p/p2p-core/src/network_zones/tor.rs @@ -0,0 +1,52 @@ +//! Tor Zone +//! +//! This module define the Tor Zone that uses the Tor network and .onion service addressing. +//! +//! ### Anonymity +//! +//! This is an anonymous network and is therefore operating under the following behavior: +//! - The node address is blend into its own address book. +//! - This network is only use for relaying transactions. +//! +//! ### Addressing +//! +//! The Tor Zone is using [`OnionAddr`] as its address type. +//! + +use cuprate_wire::network_address::OnionAddr; + +use crate::{NetZoneAddress, NetworkZone}; + +impl NetZoneAddress for OnionAddr { + type BanID = [u8; 56]; + + fn set_port(&mut self, port: u16) { + self.port = port; + } + + fn ban_id(&self) -> Self::BanID { + self.domain() + } + + fn make_canonical(&mut self) { + // There are no canonical form of an onion address... + } + + fn should_add_to_peer_list(&self) -> bool { + // Validation of the onion address has been done at the type construction... + true + } +} + +#[derive(Clone, Copy)] +pub struct Tor; + +impl NetworkZone for Tor { + const NAME: &'static str = "Tor"; + + const CHECK_NODE_ID: bool = false; + + const BROADCAST_OWN_ADDR: bool = true; + + type Addr = OnionAddr; +} diff --git a/p2p/p2p-core/src/services.rs b/p2p/p2p-core/src/services.rs index a9aee45..75f86df 100644 --- a/p2p/p2p-core/src/services.rs +++ b/p2p/p2p-core/src/services.rs @@ -115,6 +115,9 @@ pub enum AddressBookRequest { /// Gets the specified number of white peers, or less if we don't have enough. GetWhitePeers(usize), + /// Gets our own optionally specified address + OwnAddress, + /// Get info on all peers, white & grey. Peerlist, @@ -175,4 +178,10 @@ pub enum AddressBookResponse { /// Response to [`AddressBookRequest::GetBans`]. GetBans(Vec>), + + /// Response to [`AddressBookRequest::OwnAddress`] + /// + /// This returns [`None`] if the address book do + /// not contain a self designated address. + OwnAddress(Option), } diff --git a/p2p/p2p/src/config.rs b/p2p/p2p/src/config.rs index 7d2d367..2bbc0ee 100644 --- a/p2p/p2p/src/config.rs +++ b/p2p/p2p/src/config.rs @@ -28,7 +28,7 @@ pub struct P2PConfig { pub rpc_port: u16, /// The [`AddressBookConfig`]. - pub address_book_config: AddressBookConfig, + pub address_book_config: AddressBookConfig, } /// Configuration part responsible of transportation diff --git a/test-utils/src/test_netzone.rs b/test-utils/src/test_netzone.rs index 91a0f0d..c618cf1 100644 --- a/test-utils/src/test_netzone.rs +++ b/test-utils/src/test_netzone.rs @@ -52,8 +52,9 @@ impl TryFrom for TestNetZoneAddr { match value { NetworkAddress::Clear(soc) => match soc { SocketAddr::V4(v4) => Ok(Self(u32::from_be_bytes(v4.ip().octets()))), - SocketAddr::V6(_) => panic!("None v4 address in test code"), + SocketAddr::V6(_) => panic!("Only IPv4 addresses are allowed in test code."), }, + NetworkAddress::Tor(_) => panic!("Only IPv4 addresses are allowed in test code."), } } }