Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use super::{client::Client, node::Node};
use crate::chain::ChainState;
use crate::network::ConnectionType;
use crate::{BlockType, Config, FilterType};
use crate::{Socks5Proxy, TrustedPeer};
use crate::{DnsPeer, Socks5Proxy, TrustedPeer};

const MIN_PEERS: u8 = 1;
const MAX_PEERS: u8 = 15;
Expand Down Expand Up @@ -60,6 +60,20 @@ impl Builder {
self
}

/// Add a peer identified by hostname. The hostname is resolved via DNS on each
/// connection attempt, so if the IP changes between reconnections the node will
/// follow it. DNS peers are never consumed — they persist across reconnections.
pub fn add_dns_peer(mut self, peer: DnsPeer) -> Self {
self.config.dns_peers.push(peer);
self
}

/// Add multiple DNS-based peers.
pub fn add_dns_peers(mut self, peers: impl IntoIterator<Item = DnsPeer>) -> Self {
self.config.dns_peers.extend(peers);
self
}

/// Add a path to the directory where data should be stored. If none is provided, the current
/// working directory will be used.
pub fn data_dir(mut self, path: impl Into<PathBuf>) -> Self {
Expand Down
29 changes: 29 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,33 @@ impl From<SocketAddr> for TrustedPeer {
}
}

/// A peer identified by hostname, resolved via DNS on each connection attempt.
///
/// Unlike [`TrustedPeer`] (which stores a resolved IP), a `DnsPeer` stores a
/// hostname that is re-resolved every time the node needs to connect. This is
/// useful in environments where the peer's IP may change (e.g., Kubernetes
/// services, dynamic DNS).
///
/// DNS peers are never consumed from the peer list — they persist and are
/// re-resolved on every reconnection attempt.
#[derive(Debug, Clone)]
pub struct DnsPeer {
/// The hostname to resolve (e.g., `"bitcoind.default.svc.cluster.local"`).
pub hostname: String,
/// The port to connect to.
pub port: u16,
}

impl DnsPeer {
/// Create a new DNS-based peer.
pub fn new(hostname: impl Into<String>, port: u16) -> Self {
Self {
hostname: hostname.into(),
port,
}
}
}

/// Route network traffic through a Socks5 proxy, typically used by a Tor daemon.
#[derive(Debug, Clone)]
pub struct Socks5Proxy(SocketAddr);
Expand Down Expand Up @@ -342,6 +369,7 @@ enum NodeState {
struct Config {
required_peers: u8,
white_list: Vec<TrustedPeer>,
dns_peers: Vec<DnsPeer>,
data_path: Option<PathBuf>,
chain_state: Option<ChainState>,
connection_type: ConnectionType,
Expand All @@ -355,6 +383,7 @@ impl Default for Config {
Self {
required_peers: 1,
white_list: Default::default(),
dns_peers: Default::default(),
data_path: Default::default(),
chain_state: Default::default(),
connection_type: Default::default(),
Expand Down
34 changes: 31 additions & 3 deletions src/network/peer_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::{
chain::HeightMonitor,
default_port_from_network,
network::{dns::bootstrap_dns, error::PeerError, peer::Peer, PeerId, PeerTimeoutConfig},
BlockType, Dialog, TrustedPeer,
BlockType, Dialog, DnsPeer, TrustedPeer,
};

use super::{AddressBook, ConnectionType, MainThreadMessage, PeerThreadMessage};
Expand Down Expand Up @@ -57,6 +57,8 @@ pub(crate) struct PeerMap {
db: Arc<Mutex<AddressBook>>,
connector: ConnectionType,
whitelist: Whitelist,
dns_peers: Vec<DnsPeer>,
dns_peer_index: usize,
dialog: Arc<Dialog>,
timeout_config: PeerTimeoutConfig,
}
Expand All @@ -68,6 +70,7 @@ impl PeerMap {
network: Network,
block_type: BlockType,
whitelist: Whitelist,
dns_peers: Vec<DnsPeer>,
dialog: Arc<Dialog>,
connection_type: ConnectionType,
timeout_config: PeerTimeoutConfig,
Expand All @@ -84,6 +87,8 @@ impl PeerMap {
db: Arc::new(Mutex::new(AddressBook::new())),
connector: connection_type,
whitelist,
dns_peers,
dns_peer_index: 0,
dialog,
timeout_config,
}
Expand Down Expand Up @@ -229,8 +234,8 @@ impl PeerMap {
false
}

// Pull a peer from the configuration if we have one. If not, select a random peer from the database,
// as long as it is not from the same netgroup. If there are no peers in the database, try DNS.
// Pull a peer from the configuration if we have one. If not, try resolving DNS peers.
// If neither are available, select from the address book or bootstrap with DNS seeds.
pub async fn next_peer(&mut self) -> Option<Record> {
if let Some(peer) = self.whitelist.pop() {
crate::debug!("Using a configured peer");
Expand All @@ -240,6 +245,29 @@ impl PeerMap {
let record = Record::new(peer.address(), port, peer.known_services, &LOCAL_HOST);
return Some(record);
}
// Try resolving DNS peers (round-robin through all of them).
if !self.dns_peers.is_empty() {
let attempts = self.dns_peers.len();
for _ in 0..attempts {
let idx = self.dns_peer_index % self.dns_peers.len();
self.dns_peer_index = self.dns_peer_index.wrapping_add(1);
let dns_peer = &self.dns_peers[idx];
let host_port = format!("{}:{}", dns_peer.hostname, dns_peer.port);
crate::debug!(format!("Resolving DNS peer: {host_port}"));
if let Ok(mut addrs) = tokio::net::lookup_host(&host_port).await {
if let Some(addr) = addrs.next() {
crate::debug!(format!("Resolved {host_port} to {addr}"));
let ip = match addr.ip() {
IpAddr::V4(ip) => AddrV2::Ipv4(ip),
IpAddr::V6(ip) => AddrV2::Ipv6(ip),
};
let record = Record::new(ip, addr.port(), ServiceFlags::NONE, &LOCAL_HOST);
return Some(record);
}
}
crate::debug!(format!("Failed to resolve DNS peer: {host_port}"));
}
}
let mut db_lock = self.db.lock().await;
if db_lock.is_empty() {
crate::debug!("Bootstrapping peers with DNS");
Expand Down
2 changes: 2 additions & 0 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl Node {
let Config {
required_peers,
white_list,
dns_peers,
data_path: _,
chain_state,
connection_type,
Expand All @@ -95,6 +96,7 @@ impl Node {
network,
block_type,
white_list,
dns_peers,
Arc::clone(&dialog),
connection_type,
peer_timeout_config,
Expand Down
38 changes: 37 additions & 1 deletion tests/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bip157::{
chain::{checkpoints::HeaderCheckpoint, BlockHeaderChanges, ChainState},
client::Client,
node::Node,
Address, BlockHash, Event, Info, ServiceFlags, Transaction, TrustedPeer, Warning,
Address, BlockHash, DnsPeer, Event, Info, ServiceFlags, Transaction, TrustedPeer, Warning,
};
use bitcoin::{
absolute,
Expand Down Expand Up @@ -633,6 +633,42 @@ async fn tx_can_broadcast() {
.unwrap();
}

#[tokio::test]
async fn dns_peer_sync() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 10, 2).await;
let best = best_hash(rpc);
// Exercise the full DnsPeer code path: round-robin selection in next_peer(),
// resolution via tokio::net::lookup_host, Record creation, and connection.
// We use an IP literal here because "localhost" resolves to [::1] on macOS
// while bitcoind binds to IPv4 only. Real DNS resolution of hostnames is
// covered by the dns_works test above.
let dns_peer = DnsPeer::new(socket_addr.ip().to_string(), socket_addr.port());
let builder = bip157::builder::Builder::new(bitcoin::Network::Regtest)
.chain_state(ChainState::Checkpoint(HeaderCheckpoint::from_genesis(
bitcoin::Network::Regtest,
)))
.add_dns_peer(dns_peer)
.data_dir(tempdir);
let (node, client) = builder.build();
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
let cp = requester.chain_tip().await.unwrap();
assert_eq!(cp.hash, best);
requester.shutdown().unwrap();
rpc.stop().unwrap();
}

#[tokio::test]
async fn dns_works() {
let hostname = bip157::lookup_host("seed.bitcoin.sipa.be").await;
Expand Down
Loading