From ac130c247829a378030e0de6428a2ef92d1addfe Mon Sep 17 00:00:00 2001 From: russeree Date: Tue, 20 Oct 2026 07:42:31 -0700 Subject: [PATCH] net: dial a proxied DNS seed by name With --proxy the DNS seeds are queued as addr-fetch dials by host name. e56eb9d0 made prepare_outbound_session take DialTarget::net_addr() for every dial, and net_addr() expected a Domain to parse as an overlay NetAddr. A DNS name does not, so every seed dial panicked the task. A Domain that is not an overlay host now keeps the 0.0.0.0:port identity it had before that change, and the connect goes through Dialer::connect_domain, which hands overlay hosts to connect_net. Co-Authored-By: Claude Opus 5.5 --- changelog.d/proxy-seed-dns-dial.md | 7 +++ crates/rbitcoin-net/src/peers.rs | 5 +- crates/rbitcoin-net/src/service.rs | 79 +++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 changelog.d/proxy-seed-dns-dial.md diff --git a/changelog.d/proxy-seed-dns-dial.md b/changelog.d/proxy-seed-dns-dial.md new file mode 100644 index 0000000000..ffa395133a --- /dev/null +++ b/changelog.d/proxy-seed-dns-dial.md @@ -0,0 +1,7 @@ +Fixed + +- **`--proxy` no longer panics on the DNS seed addr-fetch dials.** + With a proxy, each DNS seed is dialed by name through SOCKS. The + outbound session parsed that name as an overlay address and panicked + the dial task. The seed is now dialed by name, with the `0.0.0.0:port` + peer identity it had before the overlay `NetAddr` tracking. diff --git a/crates/rbitcoin-net/src/peers.rs b/crates/rbitcoin-net/src/peers.rs index d1b52f1e17..cd958a169d 100644 --- a/crates/rbitcoin-net/src/peers.rs +++ b/crates/rbitcoin-net/src/peers.rs @@ -202,12 +202,15 @@ impl DialTarget { self.version_socket() } + /// An overlay host is its own `NetAddr`. A DNS name (a `--proxy` seed + /// addr-fetch) has no address until the proxy resolves it, so it is the + /// [`Self::version_socket`] `0.0.0.0:port`. pub fn net_addr(&self) -> crate::NetAddr { match self { Self::Socket(addr) => crate::NetAddr::from_socket(*addr), Self::Domain { host, port } => format!("{host}:{port}") .parse() - .expect("DialTarget::Domain is host:port from overlay NetAddr"), + .unwrap_or_else(|_| crate::NetAddr::from_socket(self.version_socket())), } } } diff --git a/crates/rbitcoin-net/src/service.rs b/crates/rbitcoin-net/src/service.rs index 9a74683fa2..b02b7112a6 100644 --- a/crates/rbitcoin-net/src/service.rs +++ b/crates/rbitcoin-net/src/service.rs @@ -699,9 +699,15 @@ async fn prepare_outbound_session( if peers.net_discouraged(peer_net) { return Err(NetError::Protocol("discouraged")); } + let connect = async { + match &peer { + DialTarget::Socket(_) => dialer.connect_net(peer_net).await, + DialTarget::Domain { host, port } => dialer.connect_domain(host, *port).await, + } + }; let stream = tokio::time::timeout( crate::connect_timeout_for(peer_net, OUTBOUND_CONNECT_TIMEOUT), - dialer.connect_net(peer_net), + connect, ) .await .map_err(|_| NetError::Timeout)??; @@ -948,6 +954,77 @@ mod tests { ); } + /// `--proxy` turns the DNS seeds into addr-fetch dials by name. The + /// proxy resolves the name, so the node holds no address for it. + #[tokio::test] + async fn proxied_dns_seed_dials_by_name_through_socks() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let proxy = listener.local_addr().unwrap(); + let socks = tokio::spawn(async move { + let (mut s, _) = listener.accept().await.unwrap(); + let mut greeting = [0u8; 2]; + s.read_exact(&mut greeting).await.unwrap(); + let mut methods = vec![0u8; greeting[1] as usize]; + s.read_exact(&mut methods).await.unwrap(); + if methods.contains(&0x02) { + s.write_all(&[5, 0x02]).await.unwrap(); + let mut ulen = [0u8; 2]; + s.read_exact(&mut ulen).await.unwrap(); + let mut user = vec![0u8; ulen[1] as usize]; + s.read_exact(&mut user).await.unwrap(); + let mut plen = [0u8; 1]; + s.read_exact(&mut plen).await.unwrap(); + let mut pass = vec![0u8; plen[0] as usize]; + s.read_exact(&mut pass).await.unwrap(); + s.write_all(&[1, 0]).await.unwrap(); + } else { + s.write_all(&[5, 0x00]).await.unwrap(); + } + let mut req = [0u8; 5]; + s.read_exact(&mut req).await.unwrap(); + assert_eq!(req[3], 3, "ATYP domain"); + let mut host = vec![0u8; req[4] as usize]; + s.read_exact(&mut host).await.unwrap(); + let mut port = [0u8; 2]; + s.read_exact(&mut port).await.unwrap(); + s.write_all(&[5, 0, 0, 1, 0, 0, 0, 0, 0, 0]).await.unwrap(); + (String::from_utf8(host).unwrap(), u16::from_be_bytes(port)) + }); + let peers = crate::peers::PeerHub::new(); + let (dir, hub) = crate::chain::tiny_regtest_hub_labeled("proxied-dns-seed"); + let (_tx, rx) = tokio::sync::oneshot::channel(); + let result = tokio::time::timeout( + std::time::Duration::from_secs(5), + run_outbound_session_with_abort( + crate::peers::DialTarget::Domain { + host: "seed.example".into(), + port: 8333, + }, + None, + Magic::REGTEST, + std::net::SocketAddr::from(([127, 0, 0, 1], 0)), + std::sync::Arc::new(hub), + peers, + "/rbitcoin:test/".into(), + std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)), + crate::peers::PeerConnType::AddrFetch, + rx, + crate::socks::Dialer::socks(proxy, false), + ), + ) + .await; + let _ = std::fs::remove_dir_all(dir.path()); + assert!( + result + .expect("the session ends when the proxy closes") + .is_err(), + "the proxy closed before VERSION" + ); + assert_eq!(socks.await.unwrap(), ("seed.example".to_string(), 8333)); + } + /// Dial and connect-retry block on a channel and a tick. Shutdown must /// wake them; the grace is only for a session that is still unwinding. #[tokio::test]