Skip to content

Commit 7470c44

Browse files
0xsiddharthksrustaceanrob
authored andcommitted
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 fdd1a2a commit 7470c44

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
@@ -184,6 +184,18 @@ impl std::cmp::Ord for IndexedFilter {
184184
}
185185
}
186186

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

218233
impl TrustedPeer {
219234
/// Create a new trusted peer.
220-
pub fn new(address: AddrV2, port: Option<u16>, services: ServiceFlags) -> Self {
235+
pub fn new(address: impl Into<AddrV2>, port: Option<u16>, services: ServiceFlags) -> Self {
221236
Self {
222-
address,
237+
address: TrustedPeerInner::Addr(address.into()),
223238
port,
224239
known_services: services,
225240
}
@@ -232,7 +247,7 @@ impl TrustedPeer {
232247
IpAddr::V6(ip) => AddrV2::Ipv6(ip),
233248
};
234249
Self {
235-
address,
250+
address: TrustedPeerInner::Addr(address),
236251
port: None,
237252
known_services: ServiceFlags::NONE,
238253
}
@@ -246,15 +261,24 @@ impl TrustedPeer {
246261
SocketAddr::V6(ip) => AddrV2::Ipv6(*ip.ip()),
247262
};
248263
Self {
249-
address,
264+
address: TrustedPeerInner::Addr(address),
250265
port: Some(socket_addr.port()),
251266
known_services: ServiceFlags::NONE,
252267
}
253268
}
254269

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

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

286-
impl From<TrustedPeer> for (AddrV2, Option<u16>) {
287-
fn from(value: TrustedPeer) -> Self {
288-
(value.address(), value.port())
289-
}
290-
}
291-
292310
impl From<IpAddr> for TrustedPeer {
293311
fn from(value: IpAddr) -> Self {
294312
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)