Skip to content

Commit 2699acd

Browse files
committed
feat: add DNS resolution to TrustedPeer
Introduce an enum wrapping either a pre-resolved AddrV2 or a DNS hostname, and a new TrustedPeer::from_hostname constructor. Hostnames are resolved via tokio::net::lookup_host inside PeerMap::next_peer at each attempt, so the backing IP can change between reconnections without the caller pre-resolving. Like IP-based trusted peers, hostname peers are consumed on use. Retries and re-resolution across node lifetimes are the client's responsibility (rebuild the node on NoReachablePeers). If a hostname fails to resolve, it is logged and the next whitelist entry is tried; the node only surfaces NoReachablePeers once the entire whitelist is exhausted. Useful when the backing IP of a peer may change between reconnections, e.g. a Kubernetes service whose pod IP rotates.
1 parent 3704bda commit 2699acd

3 files changed

Lines changed: 97 additions & 24 deletions

File tree

src/lib.rs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ impl std::cmp::Ord for IndexedFilter {
185185
}
186186
}
187187

188+
#[derive(Debug, Clone)]
189+
enum TrustedPeerInner {
190+
Addr(AddrV2),
191+
Hostname(String),
192+
}
193+
194+
impl From<AddrV2> for TrustedPeerInner {
195+
fn from(addr: AddrV2) -> Self {
196+
Self::Addr(addr)
197+
}
198+
}
199+
188200
/// A peer on the Bitcoin P2P network
189201
///
190202
/// # Building peers
@@ -205,22 +217,25 @@ impl std::cmp::Ord for IndexedFilter {
205217
/// // Or implicitly with `into`
206218
/// let local_host = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
207219
/// let trusted: TrustedPeer = (local_host, None).into();
220+
///
221+
/// // Or from a hostname — resolution happens at connection time.
222+
/// let trusted = TrustedPeer::from_hostname("bitcoind.svc.local", 8333);
208223
/// ```
209224
#[derive(Debug, Clone)]
210225
pub struct TrustedPeer {
211-
/// The IP address of the remote node to connect to.
212-
pub address: AddrV2,
213-
/// The port to establish a TCP connection. If none is provided, the typical Bitcoin Core port is used as the default.
214-
pub port: Option<u16>,
215-
/// The services this peer is known to offer before starting the node.
216-
pub known_services: ServiceFlags,
226+
// The address or hostname of the remote node to connect to.
227+
address: TrustedPeerInner,
228+
// The port to establish a TCP connection. If none is provided, the typical Bitcoin Core port is used as the default.
229+
port: Option<u16>,
230+
// The services this peer is known to offer before starting the node.
231+
known_services: ServiceFlags,
217232
}
218233

219234
impl TrustedPeer {
220235
/// Create a new trusted peer.
221-
pub fn new(address: AddrV2, port: Option<u16>, services: ServiceFlags) -> Self {
236+
pub fn new(address: impl Into<AddrV2>, port: Option<u16>, services: ServiceFlags) -> Self {
222237
Self {
223-
address,
238+
address: TrustedPeerInner::Addr(address.into()),
224239
port,
225240
known_services: services,
226241
}
@@ -233,7 +248,7 @@ impl TrustedPeer {
233248
IpAddr::V6(ip) => AddrV2::Ipv6(ip),
234249
};
235250
Self {
236-
address,
251+
address: TrustedPeerInner::Addr(address),
237252
port: None,
238253
known_services: ServiceFlags::NONE,
239254
}
@@ -247,15 +262,24 @@ impl TrustedPeer {
247262
SocketAddr::V6(ip) => AddrV2::Ipv6(*ip.ip()),
248263
};
249264
Self {
250-
address,
265+
address: TrustedPeerInner::Addr(address),
251266
port: Some(socket_addr.port()),
252267
known_services: ServiceFlags::NONE,
253268
}
254269
}
255270

256-
/// The IP address of the trusted peer.
257-
pub fn address(&self) -> AddrV2 {
258-
self.address.clone()
271+
/// Create a new trusted peer from a DNS hostname.
272+
///
273+
/// The hostname is stored as-is and resolved to an IP address at the
274+
/// time a connection is attempted, via [`tokio::net::lookup_host`]. If
275+
/// resolution fails or yields no addresses, the peer is skipped and
276+
/// the next configured peer is tried.
277+
pub fn from_hostname(hostname: impl Into<String>, port: u16) -> Self {
278+
Self {
279+
address: TrustedPeerInner::Hostname(hostname.into()),
280+
port: Some(port),
281+
known_services: ServiceFlags::NONE,
282+
}
259283
}
260284

261285
/// A recommended port to connect to, if there is one.
@@ -284,12 +308,6 @@ impl From<(IpAddr, Option<u16>)> for TrustedPeer {
284308
}
285309
}
286310

287-
impl From<TrustedPeer> for (AddrV2, Option<u16>) {
288-
fn from(value: TrustedPeer) -> Self {
289-
(value.address(), value.port())
290-
}
291-
}
292-
293311
impl From<IpAddr> for TrustedPeer {
294312
fn from(value: IpAddr) -> Self {
295313
TrustedPeer::from_ip(value)

src/network/peer_map.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::{
2424
broadcaster::BroadcastQueue,
2525
default_port_from_network,
2626
network::{dns::bootstrap_dns, error::PeerError, peer::Peer, PeerId, PeerTimeoutConfig},
27-
BlockType, Dialog, TrustedPeer,
27+
BlockType, Dialog, TrustedPeer, TrustedPeerInner,
2828
};
2929

3030
use super::{AddressBook, ConnectionType, MainThreadMessage, PeerThreadMessage};
@@ -217,13 +217,39 @@ impl PeerMap {
217217
// as long as it is not from the same netgroup. If there are no peers in the database, try DNS.
218218
// When `whitelist_only` is set, only whitelist peers are used.
219219
pub async fn next_peer(&mut self) -> Option<Record> {
220-
if let Some(peer) = self.whitelist.pop() {
221-
crate::debug!("Using a configured peer");
220+
while let Some(peer) = self.whitelist.pop() {
222221
let port = peer
223222
.port
224223
.unwrap_or(default_port_from_network(&self.network));
225-
let record = Record::new(peer.address(), port, peer.known_services, &LOCAL_HOST);
226-
return Some(record);
224+
let addr = match peer.address {
225+
TrustedPeerInner::Addr(addr) => addr,
226+
TrustedPeerInner::Hostname(ref host) => {
227+
crate::debug!(format!("Resolving hostname {host}:{port}"));
228+
match tokio::net::lookup_host((host.as_str(), port)).await {
229+
Ok(mut iter) => match iter.next() {
230+
Some(sa) => {
231+
crate::debug!(format!("Resolved {host} to {}", sa.ip()));
232+
match sa.ip() {
233+
IpAddr::V4(ip) => AddrV2::Ipv4(ip),
234+
IpAddr::V6(ip) => AddrV2::Ipv6(ip),
235+
}
236+
}
237+
None => {
238+
crate::debug!(format!(
239+
"Hostname {host} resolved to no addresses, skipping"
240+
));
241+
continue;
242+
}
243+
},
244+
Err(_) => {
245+
crate::debug!(format!("Failed to resolve hostname {host}"));
246+
continue;
247+
}
248+
}
249+
}
250+
};
251+
crate::debug!("Using a configured peer");
252+
return Some(Record::new(addr, port, peer.known_services, &LOCAL_HOST));
227253
}
228254
if self.whitelist_only {
229255
return None;

tests/core.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ async fn whitelist_only_sync() {
685685
assert_eq!(cp.hash, best);
686686
requester.shutdown().unwrap();
687687
rpc.stop().unwrap();
688+
// No peer available, white list only.
688689
let builder = bip157::builder::Builder::new(bitcoin::Network::Regtest)
689690
.chain_state(ChainState::Checkpoint(HeaderCheckpoint::from_genesis(
690691
bitcoin::Network::Regtest,
@@ -694,6 +695,34 @@ async fn whitelist_only_sync() {
694695
let (node, _client) = builder.build();
695696
let result = node.run().await;
696697
assert!(result.is_err());
698+
// Peer resolved from hostname.
699+
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
700+
let rpc = &bitcoind.client;
701+
let miner = rpc.new_address().unwrap();
702+
mine_blocks(rpc, &miner, 10, 2).await;
703+
let best = best_hash(rpc);
704+
let peer = TrustedPeer::from_hostname(socket_addr.ip().to_string(), socket_addr.port());
705+
let builder = bip157::builder::Builder::new(bitcoin::Network::Regtest)
706+
.chain_state(ChainState::Checkpoint(HeaderCheckpoint::from_genesis(
707+
bitcoin::Network::Regtest,
708+
)))
709+
.add_peer(peer)
710+
.whitelist_only()
711+
.data_dir(&tempdir);
712+
let (node, client) = builder.build();
713+
tokio::task::spawn(async move { node.run().await });
714+
let Client {
715+
requester,
716+
info_rx,
717+
warn_rx,
718+
event_rx: mut channel,
719+
} = client;
720+
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
721+
sync_assert(&best, &mut channel).await;
722+
let cp = requester.chain_tip().await.unwrap();
723+
assert_eq!(cp.hash, best);
724+
requester.shutdown().unwrap();
725+
rpc.stop().unwrap();
697726
}
698727

699728
#[tokio::test]

0 commit comments

Comments
 (0)