Skip to content

Commit 44fbf6a

Browse files
authored
Merge pull request #20 from 2140-dev/8-30-dns
Add DNS queries for a `Network`
2 parents b92e507 + 385f68c commit 44fbf6a

File tree

3 files changed

+236
-32
lines changed

3 files changed

+236
-32
lines changed

src/dns.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
use std::{
2+
fmt::Display,
3+
hash::{DefaultHasher, Hash, Hasher},
4+
io::Read,
5+
net::{IpAddr, Ipv4Addr, SocketAddr},
6+
time::SystemTime,
7+
};
8+
9+
use bitcoin::Network;
10+
11+
const BITCOIN_SEEDS: [&str; 9] = [
12+
"seed.bitcoin.sipa.be",
13+
"dnsseed.bluematt.me",
14+
"dnsseed.bitcoin.dashjr.org",
15+
"seed.bitcoinstats.com",
16+
"seed.bitcoin.jonasschnelli.ch",
17+
"seed.btc.petertodd.org",
18+
"seed.bitcoin.sprovoost.nl",
19+
"dnsseed.emzy.de",
20+
"seed.bitcoin.wiz.biz",
21+
];
22+
23+
const SIGNET_SEEDS: [&str; 2] = [
24+
"seed.signet.bitcoin.sprovoost.nl",
25+
"seed.signet.achownodes.xyz",
26+
];
27+
28+
const LOCAL_HOST: &str = "0.0.0.0:0";
29+
const HEADER_BYTES: usize = 12;
30+
31+
const RECURSIVE_FLAGS: [u8; 2] = [
32+
0x01, 0x00, // Default flags with recursive resolver
33+
];
34+
35+
const QTYPE: [u8; 4] = [
36+
0x00, 0x01, // QType: A Record
37+
0x00, 0x01, // IN
38+
];
39+
40+
const COUNTS: [u8; 6] = [
41+
0x00, 0x00, // ANCOUNT
42+
0x00, 0x00, // NSCOUNT
43+
0x00, 0x00, // ARCOUNT
44+
];
45+
46+
const A_RECORD: u16 = 0x01;
47+
const A_CLASS: u16 = 0x01;
48+
const EXPECTED_RDATA_LEN: u16 = 0x04;
49+
50+
/// Query DNS seeds to find potential peers.
51+
pub trait DnsQueryExt {
52+
/// Return as many potential peers as possible, potentially zero.
53+
fn query_dns_seeds(&self, resolver: impl Into<SocketAddr>) -> Vec<IpAddr>;
54+
}
55+
56+
impl DnsQueryExt for Network {
57+
fn query_dns_seeds(&self, resolver: impl Into<SocketAddr>) -> Vec<IpAddr> {
58+
let resolver = resolver.into();
59+
match self {
60+
Network::Bitcoin => do_dns_query(&BITCOIN_SEEDS, resolver),
61+
Network::Signet => do_dns_query(&SIGNET_SEEDS, resolver),
62+
_ => Vec::new(),
63+
}
64+
}
65+
}
66+
67+
fn do_dns_query(seeds: &[&str], resolver: SocketAddr) -> Vec<IpAddr> {
68+
let mut vals = Vec::new();
69+
for seed in seeds {
70+
let query = DnsQuery::new(seed, resolver);
71+
if let Ok(hosts) = query.lookup() {
72+
vals.extend(&hosts);
73+
}
74+
}
75+
vals
76+
}
77+
78+
#[derive(Debug)]
79+
struct DnsQuery {
80+
message_id: [u8; 2],
81+
message: Vec<u8>,
82+
question: Vec<u8>,
83+
resolver: SocketAddr,
84+
}
85+
86+
impl DnsQuery {
87+
fn new(seed: &str, dns_resolver: SocketAddr) -> Self {
88+
// Build a header
89+
let message_id = rand_bytes();
90+
let mut message = message_id.to_vec();
91+
message.extend(RECURSIVE_FLAGS);
92+
message.push(0x00); // QDCOUNT
93+
message.push(0x01); // QDCOUNT
94+
message.extend(COUNTS);
95+
let mut question = encode_qname(seed, None);
96+
question.extend(QTYPE);
97+
message.extend_from_slice(&question);
98+
Self {
99+
message_id,
100+
message,
101+
question,
102+
resolver: dns_resolver,
103+
}
104+
}
105+
106+
fn lookup(self) -> Result<Vec<IpAddr>, Error> {
107+
let sock = std::net::UdpSocket::bind(LOCAL_HOST)?;
108+
sock.connect(self.resolver)?;
109+
sock.send(&self.message)?;
110+
let mut response_buf = [0u8; 512];
111+
let (amt, _src) = sock.recv_from(&mut response_buf)?;
112+
if amt < HEADER_BYTES {
113+
return Err(Error::MalformedHeader);
114+
}
115+
let ips = self.parse_message(&response_buf[..amt])?;
116+
Ok(ips)
117+
}
118+
119+
fn parse_message(&self, mut response: &[u8]) -> Result<Vec<IpAddr>, Error> {
120+
let mut ips = Vec::with_capacity(10);
121+
let mut buf: [u8; 2] = [0, 0];
122+
response.read_exact(&mut buf)?; // Read 2 bytes
123+
if self.message_id != buf {
124+
return Err(Error::MessageId);
125+
}
126+
// Read flags and ignore
127+
response.read_exact(&mut buf)?; // Read 4 bytes
128+
response.read_exact(&mut buf)?; // Read 6 bytes
129+
let _qdcount = u16::from_be_bytes(buf);
130+
response.read_exact(&mut buf)?; // Read 8 bytes
131+
let ancount = u16::from_be_bytes(buf);
132+
response.read_exact(&mut buf)?; // Read 10 bytes
133+
let _nscount = u16::from_be_bytes(buf);
134+
response.read_exact(&mut buf)?; // Read 12 bytes
135+
let _arcount = u16::from_be_bytes(buf);
136+
// The question should be repeated back to us
137+
let mut buf: Vec<u8> = vec![0; self.question.len()];
138+
response.read_exact(&mut buf)?;
139+
if self.question != buf {
140+
return Err(Error::Question);
141+
}
142+
for _ in 0..ancount {
143+
let mut buf: [u8; 2] = [0, 0];
144+
// Read the compressed NAME field of the record and ignore
145+
response.read_exact(&mut buf)?;
146+
// Read the TYPE
147+
response.read_exact(&mut buf)?;
148+
let atype = u16::from_be_bytes(buf);
149+
// Read the CLASS
150+
response.read_exact(&mut buf)?;
151+
let aclass = u16::from_be_bytes(buf);
152+
let mut buf: [u8; 4] = [0, 0, 0, 0];
153+
// Read the TTL
154+
response.read_exact(&mut buf)?;
155+
let _ttl = u32::from_be_bytes(buf);
156+
let mut buf: [u8; 2] = [0, 0];
157+
// Read the RDLENGTH
158+
response.read_exact(&mut buf)?;
159+
let rdlength = u16::from_be_bytes(buf);
160+
// Read RDATA
161+
let mut rdata: Vec<u8> = vec![0; rdlength as usize];
162+
response.read_exact(&mut rdata)?;
163+
if atype == A_RECORD && aclass == A_CLASS && rdlength == EXPECTED_RDATA_LEN {
164+
ips.push(IpAddr::V4(Ipv4Addr::new(
165+
rdata[0], rdata[1], rdata[2], rdata[3],
166+
)))
167+
}
168+
}
169+
Ok(ips)
170+
}
171+
}
172+
173+
#[derive(Debug)]
174+
enum Error {
175+
MessageId,
176+
MalformedHeader,
177+
Question,
178+
Io(std::io::Error),
179+
}
180+
181+
impl Display for Error {
182+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183+
match self {
184+
Self::Question => write!(f, "question section was not repeated back."),
185+
Self::MalformedHeader => write!(f, "the response header was undersized."),
186+
Self::MessageId => write!(f, "the response ID does not match the request."),
187+
Self::Io(io) => write!(f, "std::io error: {io}"),
188+
}
189+
}
190+
}
191+
192+
impl From<std::io::Error> for Error {
193+
fn from(value: std::io::Error) -> Self {
194+
Error::Io(value)
195+
}
196+
}
197+
198+
impl std::error::Error for Error {}
199+
200+
fn encode_qname<S: AsRef<str>>(hostname: S, filter: Option<S>) -> Vec<u8> {
201+
let mut qname = Vec::new();
202+
let str = hostname.as_ref();
203+
if let Some(filter) = filter {
204+
let prefix = filter.as_ref();
205+
qname.push(prefix.len() as u8);
206+
qname.extend(prefix.as_bytes());
207+
}
208+
for label in str.split(".") {
209+
qname.push(label.len() as u8);
210+
qname.extend(label.as_bytes());
211+
}
212+
qname.push(0x00);
213+
qname
214+
}
215+
216+
fn rand_bytes() -> [u8; 2] {
217+
let mut hasher = DefaultHasher::new();
218+
SystemTime::now().hash(&mut hasher);
219+
let mut hash = hasher.finish();
220+
hash ^= hash << 13;
221+
hash ^= hash >> 17;
222+
hash ^= hash << 5;
223+
hash.to_be_bytes()[..2].try_into().expect("trivial cast")
224+
}

src/lib.rs

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ use std::{
66
time::{Duration, Instant},
77
};
88

9-
use bitcoin::Network;
109
use p2p::{message_compact_blocks::SendCmpct, ProtocolVersion, ServiceFlags};
1110

1211
pub extern crate p2p;
1312

13+
/// Make bitcoin-specific DNS queries
14+
pub mod dns;
1415
/// Automated version negotiation with remote peers
1516
pub mod handshake;
1617
/// Networking extensions
@@ -249,36 +250,6 @@ enum OutboundPing {
249250
LastReceived { then: Instant },
250251
}
251252

252-
/// DNS seed provider
253-
pub trait SeedsExt {
254-
/// List DNS seeds
255-
fn seeds(&self) -> Vec<&str>;
256-
}
257-
258-
impl SeedsExt for Network {
259-
fn seeds(&self) -> Vec<&str> {
260-
match self {
261-
Self::Bitcoin => vec![
262-
"seed.bitcoin.sipa.be",
263-
"dnsseed.bluematt.me",
264-
"dnsseed.bitcoin.dashjr.org",
265-
"seed.bitcoinstats.com",
266-
"seed.bitcoin.jonasschnelli.ch",
267-
"seed.btc.petertodd.org",
268-
"seed.bitcoin.sprovoost.nl",
269-
"dnsseed.emzy.de",
270-
"seed.bitcoin.wiz.biz",
271-
],
272-
Self::Signet => vec![
273-
"seed.signet.bitcoin.sprovoost.nl",
274-
"seed.signet.achownodes.xyz",
275-
],
276-
Self::Regtest => vec![],
277-
_ => unimplemented!(),
278-
}
279-
}
280-
}
281-
282253
#[cfg(test)]
283254
mod tests {
284255
use std::time::{Duration, Instant};

tests/std.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::net::{Ipv4Addr, SocketAddrV4};
1+
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
22

33
use bitcoin::Network;
4+
use bitcoin_p2p::dns::DnsQueryExt;
45
use corepc_node::{exe_path, P2P};
56

67
use bitcoin_p2p::handshake::ConnectionConfig;
@@ -75,3 +76,11 @@ fn maintain_connection() {
7576
reader.read_message().unwrap();
7677
bitcoind.stop().unwrap();
7778
}
79+
80+
#[test]
81+
fn dns_responds() {
82+
let network = Network::Signet;
83+
let cloudflare = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 53);
84+
let peers = network.query_dns_seeds(cloudflare);
85+
assert!(!peers.is_empty())
86+
}

0 commit comments

Comments
 (0)