Skip to content

Commit e25a695

Browse files
committed
fix(country): cache dns resolutions and trim lookup latency
1 parent 1ef3cae commit e25a695

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

src-tauri/src/dns.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
use std::collections::BTreeMap;
2+
use std::io;
3+
use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
4+
use std::sync::Mutex;
5+
use std::time::{Duration, Instant};
6+
7+
const PUBLIC_RESOLVERS: [&str; 2] = ["1.1.1.1:53", "8.8.8.8:53"];
8+
const RESPONSE_TIMEOUT: Duration = Duration::from_secs(1);
9+
const CACHE_TTL: Duration = Duration::from_secs(300);
10+
const TYPE_A: u16 = 1;
11+
12+
fn cache() -> &'static Mutex<BTreeMap<String, CachedAddresses>> {
13+
static CACHE: Mutex<BTreeMap<String, CachedAddresses>> = Mutex::new(BTreeMap::new());
14+
&CACHE
15+
}
16+
17+
struct CachedAddresses {
18+
at: Instant,
19+
addresses: Vec<SocketAddr>,
20+
}
21+
22+
pub fn resolve(netloc: &str) -> io::Result<Vec<SocketAddr>> {
23+
let (host, port) = split_netloc(netloc)?;
24+
if let Some(cached) = cached(&host) {
25+
return Ok(cached);
26+
}
27+
let usable = filter_usable(system_addresses(&host, port));
28+
let addresses = if usable.is_empty() {
29+
query_public(&host, port)
30+
} else {
31+
usable
32+
};
33+
if addresses.is_empty() {
34+
Err(io::Error::other(format!(
35+
"dns resolution failed for {host}"
36+
)))
37+
} else {
38+
remember(&host, &addresses);
39+
Ok(addresses)
40+
}
41+
}
42+
43+
fn cached(host: &str) -> Option<Vec<SocketAddr>> {
44+
let cache = cache().lock().ok()?;
45+
let cached = cache.get(host)?;
46+
if cached.at.elapsed() < CACHE_TTL {
47+
Some(cached.addresses.clone())
48+
} else {
49+
None
50+
}
51+
}
52+
53+
fn remember(host: &str, addresses: &[SocketAddr]) {
54+
if let Ok(mut cache) = cache().lock() {
55+
cache.insert(
56+
host.to_string(),
57+
CachedAddresses {
58+
at: Instant::now(),
59+
addresses: addresses.to_vec(),
60+
},
61+
);
62+
}
63+
}
64+
65+
fn split_netloc(netloc: &str) -> io::Result<(String, u16)> {
66+
let (host, port) = netloc
67+
.rsplit_once(':')
68+
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "netloc without port"))?;
69+
let port = port
70+
.parse()
71+
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid port"))?;
72+
Ok((host.to_string(), port))
73+
}
74+
75+
fn system_addresses(host: &str, port: u16) -> Vec<SocketAddr> {
76+
format!("{host}:{port}")
77+
.to_socket_addrs()
78+
.map(|addrs| addrs.collect())
79+
.unwrap_or_default()
80+
}
81+
82+
fn filter_usable(addresses: Vec<SocketAddr>) -> Vec<SocketAddr> {
83+
addresses
84+
.into_iter()
85+
.filter(|address| !address.ip().is_unspecified())
86+
.collect()
87+
}
88+
89+
fn query_public(host: &str, port: u16) -> Vec<SocketAddr> {
90+
PUBLIC_RESOLVERS
91+
.iter()
92+
.filter_map(|resolver| {
93+
query_a_records(host, resolver).map(|records| {
94+
records
95+
.into_iter()
96+
.map(|ip| SocketAddr::new(ip, port))
97+
.collect::<Vec<_>>()
98+
})
99+
})
100+
.next()
101+
.unwrap_or_default()
102+
}
103+
104+
fn query_a_records(host: &str, resolver: &str) -> Option<Vec<IpAddr>> {
105+
let socket = std::net::UdpSocket::bind("0.0.0.0:0").ok()?;
106+
socket.set_read_timeout(Some(RESPONSE_TIMEOUT)).ok()?;
107+
let query = build_query(host)?;
108+
socket.send_to(&query, resolver).ok()?;
109+
let mut buffer = [0u8; 1024];
110+
let received = socket.recv(&mut buffer).ok()?;
111+
parse_a_records(&buffer[..received]).filter(|records| !records.is_empty())
112+
}
113+
114+
fn build_query(host: &str) -> Option<Vec<u8>> {
115+
let mut query = vec![0x53, 0x1f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
116+
for label in host.split('.') {
117+
let length = label.len();
118+
if length == 0 || length > 63 {
119+
return None;
120+
}
121+
query.push(length as u8);
122+
query.extend_from_slice(label.as_bytes());
123+
}
124+
query.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0x01]);
125+
Some(query)
126+
}
127+
128+
fn parse_a_records(packet: &[u8]) -> Option<Vec<IpAddr>> {
129+
if packet.len() < 12 {
130+
return None;
131+
}
132+
let answers = u16::from_be_bytes([packet[6], packet[7]]) as usize;
133+
let mut offset = 12;
134+
while offset < packet.len() && packet[offset] != 0 {
135+
offset += packet[offset] as usize + 1;
136+
}
137+
offset += 5;
138+
let mut records = Vec::new();
139+
for _ in 0..answers {
140+
offset = skip_name(packet, offset)?;
141+
if offset + 10 > packet.len() {
142+
return None;
143+
}
144+
let record_type = u16::from_be_bytes([packet[offset], packet[offset + 1]]);
145+
let record_length = u16::from_be_bytes([packet[offset + 8], packet[offset + 9]]) as usize;
146+
let data_start = offset + 10;
147+
if data_start + record_length > packet.len() {
148+
return None;
149+
}
150+
if record_type == TYPE_A && record_length == 4 {
151+
records.push(IpAddr::V4(Ipv4Addr::new(
152+
packet[data_start],
153+
packet[data_start + 1],
154+
packet[data_start + 2],
155+
packet[data_start + 3],
156+
)));
157+
}
158+
offset = data_start + record_length;
159+
}
160+
Some(records)
161+
}
162+
163+
fn skip_name(packet: &[u8], mut offset: usize) -> Option<usize> {
164+
loop {
165+
let byte = *packet.get(offset)?;
166+
if byte & 0xC0 == 0xC0 {
167+
return Some(offset + 2);
168+
}
169+
if byte == 0 {
170+
return Some(offset + 1);
171+
}
172+
offset += byte as usize + 1;
173+
}
174+
}
175+
176+
#[cfg(test)]
177+
mod tests {
178+
use super::*;
179+
180+
#[test]
181+
fn cache_round_trips_addresses() {
182+
let host = "cache.test";
183+
assert!(cached(host).is_none());
184+
remember(
185+
host,
186+
&[SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 443)],
187+
);
188+
assert_eq!(
189+
cached(host).unwrap(),
190+
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 443)]
191+
);
192+
}
193+
194+
#[test]
195+
fn build_query_encodes_a_record_lookup() {
196+
let query = build_query("api.country.is").unwrap();
197+
assert_eq!(&query[0..2], &[0x53, 0x1f]);
198+
assert_eq!(&query[2..4], &[0x01, 0x00]);
199+
assert_eq!(
200+
&query[10..],
201+
&[
202+
3, b'a', b'p', b'i', 7, b'c', b'o', b'u', b'n', b't', b'r', b'y', 2, b'i', b's', 0,
203+
0, 1, 0, 1
204+
]
205+
);
206+
}
207+
208+
#[test]
209+
fn build_query_rejects_empty_labels() {
210+
assert!(build_query("").is_none());
211+
}
212+
213+
#[test]
214+
fn parse_a_records_skips_cname_and_reads_addresses() {
215+
let mut packet = vec![
216+
0x53, 0x1f, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
217+
];
218+
for label in "api.country.is".split('.') {
219+
packet.push(label.len() as u8);
220+
packet.extend_from_slice(label.as_bytes());
221+
}
222+
packet.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0x01]);
223+
packet.extend_from_slice(&[
224+
0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7B, 0x00, 0x00,
225+
]);
226+
packet.extend_from_slice(&[
227+
0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7B, 0x00, 0x04, 1, 2, 3, 4,
228+
]);
229+
let records = parse_a_records(&packet).unwrap();
230+
assert_eq!(records, vec![IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))]);
231+
}
232+
233+
#[test]
234+
fn parse_a_records_rejects_truncated_packets() {
235+
assert!(parse_a_records(&[0x53, 0x1f]).is_none());
236+
assert!(parse_a_records(&[]).is_none());
237+
}
238+
239+
#[test]
240+
fn filter_usable_drops_unspecified_addresses() {
241+
let addresses = vec![
242+
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 443),
243+
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 443),
244+
SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), 443),
245+
];
246+
let usable = filter_usable(addresses);
247+
assert_eq!(usable.len(), 1);
248+
assert_eq!(usable[0].ip(), IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));
249+
}
250+
251+
#[test]
252+
fn split_netloc_parses_host_and_port() {
253+
let (host, port) = split_netloc("api.country.is:443").unwrap();
254+
assert_eq!(host, "api.country.is");
255+
assert_eq!(port, 443);
256+
assert!(split_netloc("no-port").is_err());
257+
}
258+
}

0 commit comments

Comments
 (0)