1use std::collections::HashMap;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use futures::StreamExt;
7use once_cell::sync::Lazy;
8use reqwest::Client;
9use serde::Deserialize;
10use tokio::sync::RwLock;
11use tracing::{debug, instrument, warn};
12
13use super::types::RdapResponse;
14use crate::error::{Result, SeerError};
15use crate::retry::{RetryExecutor, RetryPolicy};
16use crate::validation::{describe_reserved_ip, normalize_domain};
17
18const IANA_BOOTSTRAP_DNS: &str = "https://data.iana.org/rdap/dns.json";
19const IANA_BOOTSTRAP_IPV4: &str = "https://data.iana.org/rdap/ipv4.json";
20const IANA_BOOTSTRAP_IPV6: &str = "https://data.iana.org/rdap/ipv6.json";
21const IANA_BOOTSTRAP_ASN: &str = "https://data.iana.org/rdap/asn.json";
22
23const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
27
28const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
30
31static RDAP_HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
34 Client::builder()
35 .timeout(DEFAULT_TIMEOUT)
36 .user_agent("Seer/1.0 (RDAP Client)")
37 .pool_max_idle_per_host(10)
38 .build()
39 .expect("Failed to build RDAP HTTP client - invalid configuration")
40});
41
42static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
44
45struct CachedBootstrap {
47 data: BootstrapData,
48 loaded_at: Instant,
49}
50
51impl CachedBootstrap {
52 fn new(data: BootstrapData) -> Self {
53 Self {
54 data,
55 loaded_at: Instant::now(),
56 }
57 }
58
59 fn is_expired(&self) -> bool {
60 self.loaded_at.elapsed() > BOOTSTRAP_TTL
61 }
62
63 fn age(&self) -> Duration {
64 self.loaded_at.elapsed()
65 }
66}
67
68struct BootstrapData {
72 dns: HashMap<String, Arc<str>>,
73 ipv4: Vec<(IpRange, Arc<str>)>,
74 ipv6: Vec<(IpRange, Arc<str>)>,
75 asn: Vec<(AsnRange, Arc<str>)>,
76}
77
78#[derive(Clone)]
79struct IpRange {
80 prefix: String,
81}
82
83#[derive(Clone)]
84struct AsnRange {
85 start: u32,
86 end: u32,
87}
88
89#[derive(Deserialize)]
90struct BootstrapResponse {
91 services: Vec<Vec<serde_json::Value>>,
92}
93
94#[derive(Debug, Clone)]
95pub struct RdapClient {
96 retry_policy: RetryPolicy,
97}
98
99impl Default for RdapClient {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl RdapClient {
106 pub fn new() -> Self {
108 Self {
109 retry_policy: RetryPolicy::default(),
110 }
111 }
112
113 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
117 self.retry_policy = policy;
118 self
119 }
120
121 pub fn without_retries(mut self) -> Self {
123 self.retry_policy = RetryPolicy::no_retry();
124 self
125 }
126
127 async fn ensure_bootstrap(&self) -> Result<()> {
130 {
132 let cache = BOOTSTRAP_CACHE.read().await;
133 if let Some(cached) = cache.as_ref() {
134 if !cached.is_expired() {
135 return Ok(());
136 }
137 }
138 }
139
140 let mut cache = BOOTSTRAP_CACHE.write().await;
142
143 if let Some(cached) = cache.as_ref() {
145 if !cached.is_expired() {
146 return Ok(());
147 }
148 }
149
150 debug!("Loading/refreshing RDAP bootstrap data");
152 match load_bootstrap_data_with_retry(&self.retry_policy).await {
153 Ok(data) => {
154 debug!(
155 dns_entries = data.dns.len(),
156 ipv4_entries = data.ipv4.len(),
157 ipv6_entries = data.ipv6.len(),
158 asn_entries = data.asn.len(),
159 "RDAP bootstrap loaded/refreshed"
160 );
161 *cache = Some(CachedBootstrap::new(data));
162 Ok(())
163 }
164 Err(e) => {
165 if let Some(cached) = cache.as_ref() {
167 warn!(
168 error = %e,
169 age_hours = cached.age().as_secs() / 3600,
170 "Bootstrap refresh failed, using stale data"
171 );
172 Ok(())
173 } else {
174 Err(e)
176 }
177 }
178 }
179 }
180
181 fn get_rdap_url_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<str>> {
183 let tld = domain.rsplit('.').next()?;
184 cache.dns.get(&tld.to_lowercase()).cloned()
185 }
186
187 fn get_rdap_url_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<str>> {
189 match ip {
190 IpAddr::V4(addr) => {
191 for (range, url) in &cache.ipv4 {
192 if ipv4_matches_prefix(&range.prefix, addr) {
193 return Some(Arc::clone(url));
194 }
195 }
196 }
197 IpAddr::V6(addr) => {
198 for (range, url) in &cache.ipv6 {
199 if ipv6_matches_prefix(&range.prefix, addr) {
200 return Some(Arc::clone(url));
201 }
202 }
203 }
204 }
205
206 None
207 }
208
209 fn get_rdap_url_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<str>> {
211 for (range, url) in &cache.asn {
212 if asn >= range.start && asn <= range.end {
213 return Some(Arc::clone(url));
214 }
215 }
216
217 None
218 }
219
220 #[instrument(skip(self), fields(domain = %domain))]
224 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
225 self.ensure_bootstrap().await?;
226
227 let domain = normalize_domain(domain)?;
228
229 let url = {
231 let cache_guard = BOOTSTRAP_CACHE.read().await;
232 let cache = cache_guard.as_ref().ok_or_else(|| {
233 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
234 })?;
235
236 let base_url =
237 Self::get_rdap_url_for_domain(&cache.data, &domain).ok_or_else(|| {
238 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
239 })?;
240
241 build_rdap_url(&base_url, &format!("domain/{}", domain))
242 }; debug!(url = %url, "Querying RDAP");
245 self.query_rdap_with_retry(&url).await
246 }
247
248 #[instrument(skip(self), fields(ip = %ip))]
252 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
253 self.ensure_bootstrap().await?;
254
255 let ip_addr: IpAddr = ip
256 .parse()
257 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
258
259 let url = {
261 let cache_guard = BOOTSTRAP_CACHE.read().await;
262 let cache = cache_guard.as_ref().ok_or_else(|| {
263 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
264 })?;
265
266 let base_url = Self::get_rdap_url_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
267 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
268 })?;
269
270 build_rdap_url(&base_url, &format!("ip/{}", ip))
271 }; debug!(url = %url, "Querying RDAP");
274 self.query_rdap_with_retry(&url).await
275 }
276
277 #[instrument(skip(self), fields(asn = %asn))]
281 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
282 self.ensure_bootstrap().await?;
283
284 let url = {
286 let cache_guard = BOOTSTRAP_CACHE.read().await;
287 let cache = cache_guard.as_ref().ok_or_else(|| {
288 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
289 })?;
290
291 let base_url = Self::get_rdap_url_for_asn(&cache.data, asn).ok_or_else(|| {
292 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
293 })?;
294
295 build_rdap_url(&base_url, &format!("autnum/{}", asn))
296 }; debug!(url = %url, "Querying RDAP");
299 self.query_rdap_with_retry(&url).await
300 }
301
302 #[instrument(skip(self), fields(tld = %tld))]
307 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
308 if self.ensure_bootstrap().await.is_err() {
309 return None;
310 }
311
312 let cache_guard = BOOTSTRAP_CACHE.read().await;
313 let cache = cache_guard.as_ref()?;
314 cache
315 .data
316 .dns
317 .get(&tld.to_lowercase())
318 .map(|url| url.to_string())
319 }
320
321 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
323 let executor = RetryExecutor::new(self.retry_policy.clone());
324 let url = url.to_string();
325
326 executor
327 .execute(|| {
328 let http = RDAP_HTTP_CLIENT.clone();
329 let url = url.clone();
330 async move { query_rdap_internal(&http, &url).await }
331 })
332 .await
333 }
334}
335
336const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
338
339async fn validate_url_not_reserved(url: &str) -> Result<()> {
341 let parsed = url::Url::parse(url)
342 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
343 let host = parsed
344 .host_str()
345 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
346
347 if let Ok(ip) = host.parse::<IpAddr>() {
349 if let Some(reason) = describe_reserved_ip(&ip) {
350 return Err(SeerError::RdapError(format!(
351 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
352 ip, reason
353 )));
354 }
355 return Ok(());
356 }
357
358 let port = parsed.port_or_known_default().unwrap_or(443);
359 let addr = format!("{}:{}", host, port);
360
361 let socket_addrs: Vec<_> = tokio::net::lookup_host(&addr)
362 .await
363 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
364 .collect();
365
366 if socket_addrs.is_empty() {
367 return Err(SeerError::RdapError(format!(
368 "host '{}' resolved to no addresses",
369 host
370 )));
371 }
372
373 for socket_addr in &socket_addrs {
374 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
375 return Err(SeerError::RdapError(format!(
376 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
377 socket_addr.ip(),
378 reason
379 )));
380 }
381 }
382
383 Ok(())
384}
385
386async fn query_rdap_internal(http: &Client, url: &str) -> Result<RdapResponse> {
388 validate_url_not_reserved(url).await?;
390
391 let response = http
392 .get(url)
393 .header("Accept", "application/rdap+json")
394 .send()
395 .await?;
396
397 if !response.status().is_success() {
398 return Err(SeerError::RdapError(format!(
399 "query failed with status {}",
400 response.status()
401 )));
402 }
403
404 let mut body = Vec::new();
406 let mut stream = response.bytes_stream();
407 while let Some(chunk) = stream.next().await {
408 let chunk =
409 chunk.map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
410 body.extend_from_slice(&chunk);
411 if body.len() > MAX_RDAP_RESPONSE_SIZE {
412 return Err(SeerError::RdapError(format!(
413 "RDAP response exceeds {} byte limit",
414 MAX_RDAP_RESPONSE_SIZE
415 )));
416 }
417 }
418 let rdap: RdapResponse = serde_json::from_slice(&body)?;
419 Ok(rdap)
420}
421
422async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
424 let executor = RetryExecutor::new(policy.clone());
425 executor.execute(load_bootstrap_data).await
426}
427
428async fn load_bootstrap_data() -> Result<BootstrapData> {
430 debug!("Loading RDAP bootstrap data from IANA");
431
432 let bootstrap_urls = [
434 IANA_BOOTSTRAP_DNS,
435 IANA_BOOTSTRAP_IPV4,
436 IANA_BOOTSTRAP_IPV6,
437 IANA_BOOTSTRAP_ASN,
438 ];
439 for url in &bootstrap_urls {
440 validate_url_not_reserved(url).await?;
441 }
442
443 let http = &*RDAP_HTTP_CLIENT;
444
445 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
446 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
447 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
448 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
449
450 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
451 tokio::try_join!(dns_future, ipv4_future, ipv6_future, asn_future)?;
452
453 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
457 let mut body = Vec::new();
458 let mut stream = resp.bytes_stream();
459 while let Some(chunk) = stream.next().await {
460 let chunk = chunk.map_err(|e| {
461 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
462 })?;
463 body.extend_from_slice(&chunk);
464 if body.len() > MAX_BOOTSTRAP_SIZE {
465 return Err(SeerError::RdapBootstrapError(format!(
466 "bootstrap response too large (exceeds {} bytes)",
467 MAX_BOOTSTRAP_SIZE
468 )));
469 }
470 }
471 serde_json::from_slice(&body).map_err(Into::into)
472 }
473
474 let dns_data = read_bootstrap(dns_resp).await?;
475 let ipv4_data = read_bootstrap(ipv4_resp).await?;
476 let ipv6_data = read_bootstrap(ipv6_resp).await?;
477 let asn_data = read_bootstrap(asn_resp).await?;
478
479 let mut dns = HashMap::new();
480 let mut ipv4 = Vec::new();
481 let mut ipv6 = Vec::new();
482 let mut asn = Vec::new();
483
484 for service in dns_data.services {
486 if service.len() >= 2 {
487 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
488 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
489 let url_arc: Arc<str> = Arc::from(url);
490 for tld in tlds {
491 if let Some(tld_str) = tld.as_str() {
492 dns.insert(tld_str.to_lowercase(), Arc::clone(&url_arc));
493 }
494 }
495 }
496 }
497 }
498 }
499
500 for service in ipv4_data.services {
502 if service.len() >= 2 {
503 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
504 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
505 let url_arc: Arc<str> = Arc::from(url);
506 for prefix in prefixes {
507 if let Some(prefix_str) = prefix.as_str() {
508 ipv4.push((
509 IpRange {
510 prefix: prefix_str.to_string(),
511 },
512 Arc::clone(&url_arc),
513 ));
514 }
515 }
516 }
517 }
518 }
519 }
520
521 for service in ipv6_data.services {
523 if service.len() >= 2 {
524 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
525 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
526 let url_arc: Arc<str> = Arc::from(url);
527 for prefix in prefixes {
528 if let Some(prefix_str) = prefix.as_str() {
529 ipv6.push((
530 IpRange {
531 prefix: prefix_str.to_string(),
532 },
533 Arc::clone(&url_arc),
534 ));
535 }
536 }
537 }
538 }
539 }
540 }
541
542 for service in asn_data.services {
544 if service.len() >= 2 {
545 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
546 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
547 let url_arc: Arc<str> = Arc::from(url);
548 for range in ranges {
549 if let Some(range_str) = range.as_str() {
550 if let Some((start, end)) = parse_asn_range(range_str) {
551 asn.push((AsnRange { start, end }, Arc::clone(&url_arc)));
552 }
553 }
554 }
555 }
556 }
557 }
558 }
559
560 Ok(BootstrapData {
561 dns,
562 ipv4,
563 ipv6,
564 asn,
565 })
566}
567
568fn build_rdap_url(base_url: &str, path: &str) -> String {
570 if base_url.ends_with('/') {
571 format!("{}{}", base_url, path)
572 } else {
573 format!("{}/{}", base_url, path)
574 }
575}
576
577fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
578 if let Some(pos) = range.find('-') {
579 let start = range[..pos].parse().ok()?;
580 let end = range[pos + 1..].parse().ok()?;
581 Some((start, end))
582 } else {
583 let num = range.parse().ok()?;
584 Some((num, num))
585 }
586}
587
588fn ipv4_matches_prefix(prefix: &str, ip: &Ipv4Addr) -> bool {
589 let (addr_part, mask_part) = match prefix.split_once('/') {
590 Some((a, m)) => (a, Some(m)),
591 None => (prefix, None),
592 };
593
594 let prefix_ip: Ipv4Addr = match addr_part.parse() {
595 Ok(ip) => ip,
596 Err(_) => return false,
597 };
598
599 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
600 Some(bits) if bits <= 32 => bits,
601 Some(_) => return false,
602 None => 32,
603 };
604
605 let mask = if mask_bits == 0 {
606 0
607 } else {
608 u32::MAX << (32 - mask_bits)
609 };
610
611 let ip_value = u32::from(*ip);
612 let prefix_value = u32::from(prefix_ip);
613
614 (ip_value & mask) == (prefix_value & mask)
615}
616
617fn ipv6_matches_prefix(prefix: &str, ip: &Ipv6Addr) -> bool {
618 let (addr_part, mask_part) = match prefix.split_once('/') {
619 Some((a, m)) => (a, Some(m)),
620 None => (prefix, None),
621 };
622
623 let prefix_ip: Ipv6Addr = match addr_part.parse() {
624 Ok(ip) => ip,
625 Err(_) => return false,
626 };
627
628 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
629 Some(bits) if bits <= 128 => bits,
630 Some(_) => return false,
631 None => 128,
632 };
633
634 let mask = if mask_bits == 0 {
635 0u128
636 } else {
637 u128::MAX << (128 - mask_bits)
638 };
639
640 let ip_value = ipv6_to_u128(ip);
641 let prefix_value = ipv6_to_u128(&prefix_ip);
642
643 (ip_value & mask) == (prefix_value & mask)
644}
645
646fn ipv6_to_u128(ip: &Ipv6Addr) -> u128 {
647 let segments = ip.segments();
648 let mut value = 0u128;
649 for segment in segments {
650 value = (value << 16) | segment as u128;
651 }
652 value
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 #[test]
660 fn test_default_client_has_retry_policy() {
661 let client = RdapClient::new();
662 assert_eq!(client.retry_policy.max_attempts, 3);
663 }
664
665 #[test]
666 fn test_client_without_retries() {
667 let client = RdapClient::new().without_retries();
668 assert_eq!(client.retry_policy.max_attempts, 1);
669 }
670
671 #[test]
672 fn test_client_custom_retry_policy() {
673 let policy = RetryPolicy::new().with_max_attempts(5);
674 let client = RdapClient::new().with_retry_policy(policy);
675 assert_eq!(client.retry_policy.max_attempts, 5);
676 }
677
678 #[test]
679 fn test_cached_bootstrap_expiration() {
680 let data = BootstrapData {
681 dns: HashMap::new(),
682 ipv4: Vec::new(),
683 ipv6: Vec::new(),
684 asn: Vec::new(),
685 };
686 let cached = CachedBootstrap::new(data);
687 assert!(!cached.is_expired());
689 }
690
691 #[test]
692 fn test_ipv4_prefix_matching_partial_mask() {
693 let ip_in = Ipv4Addr::new(203, 0, 114, 1);
694 let ip_out = Ipv4Addr::new(203, 0, 120, 1);
695 assert!(ipv4_matches_prefix("203.0.112.0/21", &ip_in));
696 assert!(!ipv4_matches_prefix("203.0.112.0/21", &ip_out));
697 }
698
699 #[test]
700 fn test_ipv6_prefix_matching_partial_mask() {
701 let ip_in: Ipv6Addr = "2001:db8::1".parse().unwrap();
702 let ip_out: Ipv6Addr = "2001:db9::1".parse().unwrap();
703 assert!(ipv6_matches_prefix("2001:db8::/33", &ip_in));
704 assert!(!ipv6_matches_prefix("2001:db8::/33", &ip_out));
705 }
706}