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:
SyntheticBird
2025-06-05 19:54:19 +00:00
committed by GitHub
parent 640ac1bc1c
commit 392653c659
23 changed files with 490 additions and 48 deletions

2
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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,
),
}
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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),
}
}
}

View File

@@ -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,
})
}

View 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."
);
}
}
}
}

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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,

View File

@@ -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>>,

View File

@@ -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 }

View File

@@ -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

View File

@@ -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(),

View File

@@ -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

View File

@@ -1,3 +1,5 @@
mod clear;
mod tor;
pub use clear::ClearNet;
pub use tor::Tor;

View File

@@ -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";

View 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;
}

View File

@@ -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>),
}

View File

@@ -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

View File

@@ -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."),
}
}
}