mirror of
https://github.com/MAGICGrants/cuprate-for-explorer.git
synced 2026-01-08 03:13:50 -05:00
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<InternalPeerId<OnionAddr>>` 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<Z::Addr>)`. - Defined new `Tor` `NetworkZone` * fmt * fix typo and fmt * final edits? * fix test * forgor
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Z: NetworkZone>(
|
||||
&self,
|
||||
cache_dir: &Path,
|
||||
network: Network,
|
||||
) -> cuprate_address_book::AddressBookConfig {
|
||||
our_own_address: Option<Z::Addr>,
|
||||
) -> cuprate_address_book::AddressBookConfig<Z> {
|
||||
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<SocketAddr> {
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Seed nodes for `Tor`.
|
||||
pub fn tor_net_seed_nodes(network: Network) -> Vec<OnionAddr> {
|
||||
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::<Result<_, _>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -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<<ClearNet as NetworkZone>::Addr>),
|
||||
/// A Tor onion peer.
|
||||
Tor(InternalPeerID<<Tor as NetworkZone>::Addr>),
|
||||
}
|
||||
|
||||
impl From<InternalPeerID<<ClearNet as NetworkZone>::Addr>> for CrossNetworkInternalPeerId {
|
||||
fn from(addr: InternalPeerID<<ClearNet as NetworkZone>::Addr>) -> Self {
|
||||
impl From<InternalPeerID<SocketAddr>> for CrossNetworkInternalPeerId {
|
||||
fn from(addr: InternalPeerID<SocketAddr>) -> Self {
|
||||
Self::ClearNet(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InternalPeerID<OnionAddr>> for CrossNetworkInternalPeerId {
|
||||
fn from(addr: InternalPeerID<OnionAddr>) -> Self {
|
||||
Self::Tor(addr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<NetworkAddress> for SocketAddr {
|
||||
fn try_from(value: NetworkAddress) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
NetworkAddress::Clear(addr) => Ok(addr),
|
||||
//_ => Err(NetworkAddressIncorrectZone)
|
||||
NetworkAddress::Tor(_) => Err(NetworkAddressIncorrectZone),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<u8>,
|
||||
/// Type of the network address (used later for conversion)
|
||||
ty: Option<AddressType>,
|
||||
/// All possible fields for a network address
|
||||
addr: Option<AllFieldsNetworkAddress>,
|
||||
}
|
||||
|
||||
epee_object!(
|
||||
TaggedNetworkAddress,
|
||||
ty("type"): Option<u8>,
|
||||
ty("type"): Option<AddressType>,
|
||||
addr: Option<AllFieldsNetworkAddress>,
|
||||
);
|
||||
|
||||
@@ -75,31 +91,57 @@ impl From<NetworkAddress> 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<u32>,
|
||||
/// IP port field
|
||||
m_port: Option<u16>,
|
||||
|
||||
/// IPv6 address
|
||||
addr: Option<[u8; 16]>,
|
||||
|
||||
/// Alternative network domain name (<domain>.onion or <domain>.i2p)
|
||||
host: Option<String>,
|
||||
/// Alternative network virtual port
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
epee_object!(
|
||||
@@ -107,21 +149,27 @@ epee_object!(
|
||||
m_ip: Option<u32>,
|
||||
m_port: Option<u16>,
|
||||
addr: Option<[u8; 16]>,
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
);
|
||||
|
||||
impl AllFieldsNetworkAddress {
|
||||
fn try_into_network_address(self, ty: u8) -> Option<NetworkAddress> {
|
||||
fn try_into_network_address(self, ty: AddressType) -> Option<NetworkAddress> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
233
net/wire/src/network_address/onion_addr.rs
Normal file
233
net/wire/src/network_address/onion_addr.rs
Normal file
@@ -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
|
||||
/// <https://spec.torproject.org/rend-spec/encoding-onion-addresses.html>
|
||||
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, OnionAddrParsingError> {
|
||||
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 `<domain>.onion:<port>`
|
||||
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<Self, Self::Err> {
|
||||
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<NetworkAddress> for OnionAddr {
|
||||
type Error = NetworkAddressIncorrectZone;
|
||||
fn try_from(value: NetworkAddress) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
NetworkAddress::Tor(addr) => Ok(addr),
|
||||
NetworkAddress::Clear(_) => Err(NetworkAddressIncorrectZone),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OnionAddr> 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::<u16>(), 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::<OnionAddr>().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::<OnionAddr>().is_err(),
|
||||
"Address {addr} has been reported as valid."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,12 +66,12 @@ pub struct AddressBook<Z: BorshNetworkZone> {
|
||||
peer_save_task_handle: Option<JoinHandle<std::io::Result<()>>>,
|
||||
peer_save_interval: Interval,
|
||||
|
||||
cfg: AddressBookConfig,
|
||||
cfg: AddressBookConfig<Z>,
|
||||
}
|
||||
|
||||
impl<Z: BorshNetworkZone> AddressBook<Z> {
|
||||
pub fn new(
|
||||
cfg: AddressBookConfig,
|
||||
cfg: AddressBookConfig<Z>,
|
||||
white_peers: Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
gray_peers: Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
anchor_peers: Vec<Z::Addr>,
|
||||
@@ -417,6 +417,9 @@ impl<Z: BorshNetworkZone> Service<AddressBookRequest<Z>> for AddressBook<Z> {
|
||||
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
|
||||
|
||||
@@ -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<Z: NetworkZone>() -> AddressBookConfig<Z> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ mod store;
|
||||
|
||||
/// The address book config.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressBookConfig {
|
||||
pub struct AddressBookConfig<Z: NetworkZone> {
|
||||
/// 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<Z::Addr>,
|
||||
}
|
||||
|
||||
/// 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<Z: BorshNetworkZone>(
|
||||
cfg: AddressBookConfig,
|
||||
cfg: AddressBookConfig<Z>,
|
||||
) -> Result<book::AddressBook<Z>, std::io::Error> {
|
||||
let (white_list, gray_list) = match store::read_peers_from_disk::<Z>(&cfg).await {
|
||||
Ok(res) => res,
|
||||
|
||||
@@ -27,7 +27,7 @@ struct DeserPeerDataV1<A: NetZoneAddress> {
|
||||
}
|
||||
|
||||
pub(crate) fn save_peers_to_disk<Z: BorshNetworkZone>(
|
||||
cfg: &AddressBookConfig,
|
||||
cfg: &AddressBookConfig<Z>,
|
||||
white_list: &PeerList<Z>,
|
||||
gray_list: &PeerList<Z>,
|
||||
) -> JoinHandle<std::io::Result<()>> {
|
||||
@@ -51,7 +51,7 @@ pub(crate) fn save_peers_to_disk<Z: BorshNetworkZone>(
|
||||
}
|
||||
|
||||
pub(crate) async fn read_peers_from_disk<Z: BorshNetworkZone>(
|
||||
cfg: &AddressBookConfig,
|
||||
cfg: &AddressBookConfig<Z>,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ impl<N: NetworkZone> Service<AddressBookRequest<N>> for DummyAddressBook {
|
||||
AddressBookRequest::GetBan(_) => AddressBookResponse::GetBan {
|
||||
unban_instant: None,
|
||||
},
|
||||
AddressBookRequest::OwnAddress => AddressBookResponse::OwnAddress(None),
|
||||
AddressBookRequest::Peerlist
|
||||
| AddressBookRequest::PeerlistSize
|
||||
| AddressBookRequest::ConnectionCount
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod clear;
|
||||
mod tor;
|
||||
|
||||
pub use clear::ClearNet;
|
||||
pub use tor::Tor;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
52
p2p/p2p-core/src/network_zones/tor.rs
Normal file
52
p2p/p2p-core/src/network_zones/tor.rs
Normal file
@@ -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;
|
||||
}
|
||||
@@ -115,6 +115,9 @@ pub enum AddressBookRequest<Z: NetworkZone> {
|
||||
/// 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<Z: NetworkZone> {
|
||||
|
||||
/// Response to [`AddressBookRequest::GetBans`].
|
||||
GetBans(Vec<BanState<Z::Addr>>),
|
||||
|
||||
/// Response to [`AddressBookRequest::OwnAddress`]
|
||||
///
|
||||
/// This returns [`None`] if the address book do
|
||||
/// not contain a self designated address.
|
||||
OwnAddress(Option<Z::Addr>),
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct P2PConfig<Z: NetworkZone> {
|
||||
pub rpc_port: u16,
|
||||
|
||||
/// The [`AddressBookConfig`].
|
||||
pub address_book_config: AddressBookConfig,
|
||||
pub address_book_config: AddressBookConfig<Z>,
|
||||
}
|
||||
|
||||
/// Configuration part responsible of transportation
|
||||
|
||||
@@ -52,8 +52,9 @@ impl TryFrom<NetworkAddress> 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."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user