Skip to content

Commit

Permalink
add hostname to --connect
Browse files Browse the repository at this point in the history
  • Loading branch information
Davidson-Souza committed Nov 25, 2024
1 parent a1c2359 commit 609e43d
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 25 deletions.
3 changes: 2 additions & 1 deletion crates/floresta-electrum/src/electrum_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,8 @@ mod test {
chain.clone(),
Arc::new(tokio::sync::RwLock::new(Mempool::new())),
None,
);
)
.unwrap();

let node_interface = chain_provider.get_handle();

Expand Down
27 changes: 27 additions & 0 deletions crates/floresta-wire/src/p2p_wire/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::{self};
use std::io;

use floresta_chain::BlockchainError;
Expand Down Expand Up @@ -36,6 +39,8 @@ pub enum WireError {
CompactBlockFiltersError(IteratableFilterStoreError),
#[error("Poisoned lock")]
PoisonedLock,
#[error("We couldn't parse the provided address due to: {0}")]
InvalidAddress(AddrParseError),
}

impl_error_from!(WireError, PeerError, PeerError);
Expand All @@ -45,6 +50,7 @@ impl_error_from!(
IteratableFilterStoreError,
CompactBlockFiltersError
);
impl_error_from!(WireError, AddrParseError, InvalidAddress);

impl From<tokio::sync::mpsc::error::SendError<NodeRequest>> for WireError {
fn from(error: tokio::sync::mpsc::error::SendError<NodeRequest>) -> Self {
Expand All @@ -57,3 +63,24 @@ impl From<io::Error> for WireError {
WireError::Io(err)
}
}

#[derive(Debug, Clone)]
pub enum AddrParseError {
InvalidIpv6,
InvalidIpv4,
InvalidHostname,
InvalidPort,
Inconclusive,
}

impl Display for AddrParseError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
AddrParseError::InvalidIpv6 => write!(f, "Invalid ipv6"),
AddrParseError::InvalidIpv4 => write!(f, "Invalid ipv4"),
AddrParseError::InvalidHostname => write!(f, "Invalid hostname"),
AddrParseError::InvalidPort => write!(f, "Invalid port"),
AddrParseError::Inconclusive => write!(f, "Inconclusive"),
}
}
}
4 changes: 1 addition & 3 deletions crates/floresta-wire/src/p2p_wire/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ use std::net::SocketAddr;
use bitcoin::Network;
use floresta_chain::AssumeUtreexoValue;

use self::address_man::LocalAddress;

#[derive(Debug, Clone)]
/// Configuration for the Utreexo node.
pub struct UtreexoNodeConfig {
Expand All @@ -30,7 +28,7 @@ pub struct UtreexoNodeConfig {
///
/// If you want to connect to a specific peer, you can set this to a string with the
/// format `ip:port`. For example, `localhost:8333`.
pub fixed_peer: Option<LocalAddress>,
pub fixed_peer: Option<String>,
/// Maximum ban score. Defaults to 100.
///
/// If a peer misbehaves, we increase its ban score. If the ban score reaches this value,
Expand Down
215 changes: 211 additions & 4 deletions crates/floresta-wire/src/p2p_wire/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ use tokio::time::timeout;
use super::address_man::AddressMan;
use super::address_man::AddressState;
use super::address_man::LocalAddress;
use super::error::AddrParseError;
use super::error::WireError;
use super::mempool::Mempool;
use super::node_context::NodeContext;
Expand Down Expand Up @@ -207,10 +208,19 @@ where
chain: Chain,
mempool: Arc<RwLock<Mempool>>,
block_filters: Option<Arc<NetworkFilters<FlatFiltersStore>>>,
) -> Self {
) -> Result<Self, WireError> {
let (node_tx, node_rx) = unbounded_channel();
let socks5 = config.proxy.map(Socks5StreamBuilder::new);
UtreexoNode(

let fixed_peer = config
.fixed_peer
.as_ref()
.map(|address| {
Self::resolve_connect_host(&address, Self::get_port(config.network.into()))
})
.transpose()?;

Ok(UtreexoNode(
NodeCommon {
last_filter: chain.get_block_hash(0).unwrap(),
block_filters,
Expand All @@ -237,11 +247,128 @@ where
datadir: config.datadir.clone(),
socks5,
max_banscore: config.max_banscore,
fixed_peer: config.fixed_peer.clone(),
fixed_peer,
config,
},
T::default(),
)
))
}

fn get_port(network: Network) -> u16 {
match network {
Network::Bitcoin => 8333,
Network::Signet => 38333,
Network::Testnet => 18333,
Network::Regtest => 18444,
}
}

/// Resolves a string address into a LocalAddress
///
/// This function should get an address in the format `<address>[<:port>]` and return a
/// usable [`LocalAddress`]. It can be an ipv4, ipv6 or a hostname. In case of hostnames,
/// we resolve them using the system's DNS resolver and return an ip address. Errors if
/// the provided address is invalid, or we can't resolve it.
///
/// TODO: Allow for non-clearnet addresses like onion services and i2p.
fn resolve_connect_host(
address: &str,
default_port: u16,
) -> Result<LocalAddress, AddrParseError> {
// ipv6
if address.starts_with('[') {
if !address.contains(']') {
return Err(AddrParseError::InvalidIpv6);
}

let mut split = address.trim_end().split(']');
let hostname = split.next().ok_or(AddrParseError::InvalidIpv6)?;
let port = split
.next()
.filter(|x| !x.is_empty())
.map(|port| {
port.trim_start_matches(':')
.parse()
.map_err(|_e| AddrParseError::InvalidPort)
})
.transpose()?
.unwrap_or(default_port);

let hostname = hostname.trim_start_matches('[');
let ip = hostname.parse().map_err(|_e| AddrParseError::InvalidIpv6)?;
return Ok(LocalAddress::new(
AddrV2::Ipv6(ip),
0,
AddressState::NeverTried,
ServiceFlags::NONE,
port,
rand::random(),
));
}

// ipv4 - it's hard to differentiate between ipv4 and hostname without an actual regex
// simply try to parse it as an ip address and if it fails, assume it's a hostname
let mut split = address.split(':');
let ip = split
.next()
.ok_or(AddrParseError::InvalidIpv4)?
.parse()
.map_err(|_e| AddrParseError::InvalidIpv4);

match ip {
Ok(ip) => {
let port = split
.next()
.map(|port| port.parse().map_err(|_e| AddrParseError::InvalidPort))
.transpose()?
.unwrap_or(default_port);

if split.next().is_some() {
return Err(AddrParseError::Inconclusive);
}

let id = rand::random();
Ok(LocalAddress::new(
AddrV2::Ipv4(ip),
0,
AddressState::NeverTried,
ServiceFlags::NONE,
port,
id,
))
}

Err(_) => {
let mut split = address.split(':');
let hostname = split.next().ok_or(AddrParseError::InvalidHostname)?;
let port = split
.next()
.map(|port| port.parse().map_err(|_e| AddrParseError::InvalidPort))
.transpose()?
.unwrap_or(default_port);

if split.next().is_some() {
return Err(AddrParseError::Inconclusive);
}

let ip = dns_lookup::lookup_host(hostname)
.map_err(|_e| AddrParseError::InvalidHostname)?;
let id = rand::random();
let ip = match ip[0] {
std::net::IpAddr::V4(ip) => AddrV2::Ipv4(ip),
std::net::IpAddr::V6(ip) => AddrV2::Ipv6(ip),
};

Ok(LocalAddress::new(
ip,
0,
AddressState::NeverTried,
ServiceFlags::NONE,
port,
id,
))
}
}
}

pub(crate) fn get_peer_info(&self, peer: &u32) -> Option<PeerInfo> {
Expand Down Expand Up @@ -908,6 +1035,7 @@ macro_rules! try_and_log {
}
};
}

macro_rules! periodic_job {
($what:expr, $timer:expr, $interval:ident, $context:ty) => {
if $timer.elapsed() > Duration::from_secs(<$context>::$interval) {
Expand All @@ -922,5 +1050,84 @@ macro_rules! periodic_job {
}
};
}

pub(crate) use periodic_job;
pub(crate) use try_and_log;

#[cfg(test)]
mod tests {
use floresta_chain::pruned_utreexo::partial_chain::PartialChainState;

use crate::node::UtreexoNode;
use crate::running_node::RunningNode;

fn check_address_resolving(address: &str, port: u16, should_succeed: bool, description: &str) {
let result =
UtreexoNode::<RunningNode, PartialChainState>::resolve_connect_host(address, port);
if should_succeed {
assert!(result.is_ok(), "Failed: {}", description);
} else {
assert!(result.is_err(), "Unexpected success: {}", description);
}
}

#[test]
fn test_parse_address() {
// IPv6 Tests
check_address_resolving("[::1]", 8333, true, "Valid IPv6 without port");
check_address_resolving("[::1", 8333, false, "Invalid IPv6 format");
check_address_resolving("[::1]:8333", 8333, true, "Valid IPv6 with port");
check_address_resolving(
"[::1]:8333:8333",
8333,
false,
"Invalid IPv6 with multiple ports",
);

// IPv4 Tests
check_address_resolving("127.0.0.1", 8333, true, "Valid IPv4 without port");
check_address_resolving("321.321.321.321", 8333, false, "Invalid IPv4 format");
check_address_resolving("127.0.0.1:8333", 8333, true, "Valid IPv4 with port");
check_address_resolving(
"127.0.0.1:8333:8333",
8333,
false,
"Invalid IPv4 with multiple ports",
);

// Hostname Tests
check_address_resolving("example.com", 8333, true, "Valid hostname without port");
check_address_resolving("example", 8333, false, "Invalid hostname");
check_address_resolving("example.com:8333", 8333, true, "Valid hostname with port");
check_address_resolving(
"example.com:8333:8333",
8333,
false,
"Invalid hostname with multiple ports",
);

// Edge Cases
#[cfg(not(target_os = "windows"))]
check_address_resolving("", 8333, false, "Empty string address");
#[cfg(not(target_os = "windows"))]
check_address_resolving(
" 127.0.0.1:8333 ",
8333,
false,
"Address with leading/trailing spaces",
);
check_address_resolving("127.0.0.1:0", 0, true, "Valid address with port 0");
check_address_resolving(
"127.0.0.1:65535",
65535,
true,
"Valid address with maximum port",
);
check_address_resolving(
"127.0.0.1:65536",
65535,
false,
"Valid address with out-of-range port",
)
}
}
4 changes: 3 additions & 1 deletion crates/floresta-wire/src/p2p_wire/running_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,9 @@ where
chain,
self.mempool.clone(),
None,
);
)
.expect("Failed to create backfill node"); // expect is fine here, because we already
// validated this config before creating the RunningNode

UtreexoNode::<SyncNode, PartialChainState>::run(
&mut backfill,
Expand Down
3 changes: 2 additions & 1 deletion crates/floresta-wire/src/p2p_wire/tests/sync_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ mod tests_utils {
chain.clone(),
mempool,
None,
);
)
.unwrap();

for (i, peer) in peers.into_iter().enumerate() {
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
Expand Down
3 changes: 2 additions & 1 deletion crates/floresta/examples/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ async fn main() {
chain.clone(),
Arc::new(RwLock::new(Mempool::new())),
None,
);
)
.unwrap();
// A handle is a simple way to interact with the node. It implements a queue of requests
// that will be processed by the node.
let handle = p2p.get_handle();
Expand Down
Loading

0 comments on commit 609e43d

Please sign in to comment.