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(15);
28
29const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
32
33const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
35
36static RDAP_HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
39 Client::builder()
40 .timeout(DEFAULT_TIMEOUT)
41 .connect_timeout(CONNECT_TIMEOUT)
42 .user_agent("Seer/1.0 (RDAP Client)")
43 .pool_max_idle_per_host(10)
44 .build()
45 .expect("Failed to build RDAP HTTP client - invalid configuration")
46});
47
48static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
50
51struct CachedBootstrap {
53 data: BootstrapData,
54 loaded_at: Instant,
55}
56
57impl CachedBootstrap {
58 fn new(data: BootstrapData) -> Self {
59 Self {
60 data,
61 loaded_at: Instant::now(),
62 }
63 }
64
65 fn is_expired(&self) -> bool {
66 self.loaded_at.elapsed() > BOOTSTRAP_TTL
67 }
68
69 fn age(&self) -> Duration {
70 self.loaded_at.elapsed()
71 }
72}
73
74struct BootstrapData {
78 dns: HashMap<String, Arc<str>>,
79 ipv4: Vec<(IpRange, Arc<str>)>,
80 ipv6: Vec<(IpRange, Arc<str>)>,
81 asn: Vec<(AsnRange, Arc<str>)>,
82}
83
84#[derive(Clone)]
85struct IpRange {
86 prefix: String,
87}
88
89#[derive(Clone)]
90struct AsnRange {
91 start: u32,
92 end: u32,
93}
94
95#[derive(Deserialize)]
96struct BootstrapResponse {
97 services: Vec<Vec<serde_json::Value>>,
98}
99
100#[derive(Debug, Clone)]
101pub struct RdapClient {
102 retry_policy: RetryPolicy,
103}
104
105impl Default for RdapClient {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl RdapClient {
112 pub fn new() -> Self {
114 Self {
115 retry_policy: RetryPolicy::default().with_max_attempts(2),
116 }
117 }
118
119 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
123 self.retry_policy = policy;
124 self
125 }
126
127 pub fn without_retries(mut self) -> Self {
129 self.retry_policy = RetryPolicy::no_retry();
130 self
131 }
132
133 async fn ensure_bootstrap(&self) -> Result<()> {
136 {
138 let cache = BOOTSTRAP_CACHE.read().await;
139 if let Some(cached) = cache.as_ref() {
140 if !cached.is_expired() {
141 return Ok(());
142 }
143 }
144 }
145
146 let mut cache = BOOTSTRAP_CACHE.write().await;
148
149 if let Some(cached) = cache.as_ref() {
151 if !cached.is_expired() {
152 return Ok(());
153 }
154 }
155
156 debug!("Loading/refreshing RDAP bootstrap data");
158 match load_bootstrap_data_with_retry(&self.retry_policy).await {
159 Ok(data) => {
160 debug!(
161 dns_entries = data.dns.len(),
162 ipv4_entries = data.ipv4.len(),
163 ipv6_entries = data.ipv6.len(),
164 asn_entries = data.asn.len(),
165 "RDAP bootstrap loaded/refreshed"
166 );
167 *cache = Some(CachedBootstrap::new(data));
168 Ok(())
169 }
170 Err(e) => {
171 if let Some(cached) = cache.as_ref() {
173 warn!(
174 error = %e,
175 age_hours = cached.age().as_secs() / 3600,
176 "Bootstrap refresh failed, using stale data"
177 );
178 Ok(())
179 } else {
180 Err(e)
182 }
183 }
184 }
185 }
186
187 fn get_rdap_url_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<str>> {
189 let tld = domain.rsplit('.').next()?;
190 cache.dns.get(&tld.to_lowercase()).cloned()
191 }
192
193 fn get_rdap_url_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<str>> {
195 match ip {
196 IpAddr::V4(addr) => {
197 for (range, url) in &cache.ipv4 {
198 if ipv4_matches_prefix(&range.prefix, addr) {
199 return Some(Arc::clone(url));
200 }
201 }
202 }
203 IpAddr::V6(addr) => {
204 for (range, url) in &cache.ipv6 {
205 if ipv6_matches_prefix(&range.prefix, addr) {
206 return Some(Arc::clone(url));
207 }
208 }
209 }
210 }
211
212 None
213 }
214
215 fn get_rdap_url_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<str>> {
217 for (range, url) in &cache.asn {
218 if asn >= range.start && asn <= range.end {
219 return Some(Arc::clone(url));
220 }
221 }
222
223 None
224 }
225
226 #[instrument(skip(self), fields(domain = %domain))]
230 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
231 self.ensure_bootstrap().await?;
232
233 let domain = normalize_domain(domain)?;
234
235 let url = {
237 let cache_guard = BOOTSTRAP_CACHE.read().await;
238 let cache = cache_guard.as_ref().ok_or_else(|| {
239 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
240 })?;
241
242 let base_url =
243 Self::get_rdap_url_for_domain(&cache.data, &domain).ok_or_else(|| {
244 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
245 })?;
246
247 build_rdap_url(&base_url, &format!("domain/{}", domain))
248 }; debug!(url = %url, "Querying RDAP");
251 self.query_rdap_with_retry(&url).await
252 }
253
254 #[instrument(skip(self), fields(ip = %ip))]
258 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
259 self.ensure_bootstrap().await?;
260
261 let ip_addr: IpAddr = ip
262 .parse()
263 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
264
265 let url = {
267 let cache_guard = BOOTSTRAP_CACHE.read().await;
268 let cache = cache_guard.as_ref().ok_or_else(|| {
269 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
270 })?;
271
272 let base_url = Self::get_rdap_url_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
273 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
274 })?;
275
276 build_rdap_url(&base_url, &format!("ip/{}", ip))
277 }; debug!(url = %url, "Querying RDAP");
280 self.query_rdap_with_retry(&url).await
281 }
282
283 #[instrument(skip(self), fields(asn = %asn))]
287 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
288 self.ensure_bootstrap().await?;
289
290 let url = {
292 let cache_guard = BOOTSTRAP_CACHE.read().await;
293 let cache = cache_guard.as_ref().ok_or_else(|| {
294 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
295 })?;
296
297 let base_url = Self::get_rdap_url_for_asn(&cache.data, asn).ok_or_else(|| {
298 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
299 })?;
300
301 build_rdap_url(&base_url, &format!("autnum/{}", asn))
302 }; debug!(url = %url, "Querying RDAP");
305 self.query_rdap_with_retry(&url).await
306 }
307
308 #[instrument(skip(self), fields(tld = %tld))]
313 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
314 if self.ensure_bootstrap().await.is_err() {
315 return None;
316 }
317
318 let cache_guard = BOOTSTRAP_CACHE.read().await;
319 let cache = cache_guard.as_ref()?;
320 cache
321 .data
322 .dns
323 .get(&tld.to_lowercase())
324 .map(|url| url.to_string())
325 }
326
327 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
329 let executor = RetryExecutor::new(self.retry_policy.clone());
330 let url = url.to_string();
331
332 executor
333 .execute(|| {
334 let http = RDAP_HTTP_CLIENT.clone();
335 let url = url.clone();
336 async move { query_rdap_internal(&http, &url).await }
337 })
338 .await
339 }
340}
341
342const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
344
345async fn validate_url_not_reserved(url: &str) -> Result<()> {
347 let parsed = url::Url::parse(url)
348 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
349 let host = parsed
350 .host_str()
351 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
352
353 if let Ok(ip) = host.parse::<IpAddr>() {
355 if let Some(reason) = describe_reserved_ip(&ip) {
356 return Err(SeerError::RdapError(format!(
357 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
358 ip, reason
359 )));
360 }
361 return Ok(());
362 }
363
364 let port = parsed.port_or_known_default().unwrap_or(443);
365 let addr = format!("{}:{}", host, port);
366
367 let socket_addrs: Vec<_> = tokio::net::lookup_host(&addr)
368 .await
369 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
370 .collect();
371
372 if socket_addrs.is_empty() {
373 return Err(SeerError::RdapError(format!(
374 "host '{}' resolved to no addresses",
375 host
376 )));
377 }
378
379 for socket_addr in &socket_addrs {
380 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
381 return Err(SeerError::RdapError(format!(
382 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
383 socket_addr.ip(),
384 reason
385 )));
386 }
387 }
388
389 Ok(())
390}
391
392async fn query_rdap_internal(http: &Client, url: &str) -> Result<RdapResponse> {
394 validate_url_not_reserved(url).await?;
396
397 let response = http
398 .get(url)
399 .header("Accept", "application/rdap+json")
400 .send()
401 .await?;
402
403 if !response.status().is_success() {
404 return Err(SeerError::RdapError(format!(
405 "query failed with status {}",
406 response.status()
407 )));
408 }
409
410 let mut body = Vec::new();
412 let mut stream = response.bytes_stream();
413 while let Some(chunk) = stream.next().await {
414 let chunk =
415 chunk.map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
416 body.extend_from_slice(&chunk);
417 if body.len() > MAX_RDAP_RESPONSE_SIZE {
418 return Err(SeerError::RdapError(format!(
419 "RDAP response exceeds {} byte limit",
420 MAX_RDAP_RESPONSE_SIZE
421 )));
422 }
423 }
424 let rdap: RdapResponse = serde_json::from_slice(&body)?;
425 Ok(rdap)
426}
427
428async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
430 let executor = RetryExecutor::new(policy.clone());
431 executor.execute(load_bootstrap_data).await
432}
433
434async fn load_bootstrap_data() -> Result<BootstrapData> {
436 debug!("Loading RDAP bootstrap data from IANA");
437
438 let http = &*RDAP_HTTP_CLIENT;
442
443 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
444 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
445 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
446 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
447
448 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
451 tokio::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 = match dns_resp {
476 Ok(resp) => match read_bootstrap(resp).await {
477 Ok(data) => Some(data),
478 Err(e) => {
479 warn!(error = %e, "Failed to parse DNS bootstrap response");
480 None
481 }
482 },
483 Err(e) => {
484 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
485 None
486 }
487 };
488 let ipv4_data = match ipv4_resp {
489 Ok(resp) => match read_bootstrap(resp).await {
490 Ok(data) => Some(data),
491 Err(e) => {
492 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
493 None
494 }
495 },
496 Err(e) => {
497 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
498 None
499 }
500 };
501 let ipv6_data = match ipv6_resp {
502 Ok(resp) => match read_bootstrap(resp).await {
503 Ok(data) => Some(data),
504 Err(e) => {
505 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
506 None
507 }
508 },
509 Err(e) => {
510 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
511 None
512 }
513 };
514 let asn_data = match asn_resp {
515 Ok(resp) => match read_bootstrap(resp).await {
516 Ok(data) => Some(data),
517 Err(e) => {
518 warn!(error = %e, "Failed to parse ASN bootstrap response");
519 None
520 }
521 },
522 Err(e) => {
523 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
524 None
525 }
526 };
527
528 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
530 return Err(SeerError::RdapBootstrapError(
531 "all IANA bootstrap registries failed".to_string(),
532 ));
533 }
534
535 let mut dns = HashMap::new();
536 let mut ipv4 = Vec::new();
537 let mut ipv6 = Vec::new();
538 let mut asn = Vec::new();
539
540 if let Some(dns_data) = dns_data {
542 for service in dns_data.services {
543 if service.len() >= 2 {
544 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
545 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
546 let url_arc: Arc<str> = Arc::from(url);
547 for tld in tlds {
548 if let Some(tld_str) = tld.as_str() {
549 dns.insert(tld_str.to_lowercase(), Arc::clone(&url_arc));
550 }
551 }
552 }
553 }
554 }
555 }
556 }
557
558 if let Some(ipv4_data) = ipv4_data {
560 for service in ipv4_data.services {
561 if service.len() >= 2 {
562 if let (Some(prefixes), Some(urls)) =
563 (service[0].as_array(), service[1].as_array())
564 {
565 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
566 let url_arc: Arc<str> = Arc::from(url);
567 for prefix in prefixes {
568 if let Some(prefix_str) = prefix.as_str() {
569 ipv4.push((
570 IpRange {
571 prefix: prefix_str.to_string(),
572 },
573 Arc::clone(&url_arc),
574 ));
575 }
576 }
577 }
578 }
579 }
580 }
581 }
582
583 if let Some(ipv6_data) = ipv6_data {
585 for service in ipv6_data.services {
586 if service.len() >= 2 {
587 if let (Some(prefixes), Some(urls)) =
588 (service[0].as_array(), service[1].as_array())
589 {
590 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
591 let url_arc: Arc<str> = Arc::from(url);
592 for prefix in prefixes {
593 if let Some(prefix_str) = prefix.as_str() {
594 ipv6.push((
595 IpRange {
596 prefix: prefix_str.to_string(),
597 },
598 Arc::clone(&url_arc),
599 ));
600 }
601 }
602 }
603 }
604 }
605 }
606 }
607
608 if let Some(asn_data) = asn_data {
610 for service in asn_data.services {
611 if service.len() >= 2 {
612 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array())
613 {
614 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
615 let url_arc: Arc<str> = Arc::from(url);
616 for range in ranges {
617 if let Some(range_str) = range.as_str() {
618 if let Some((start, end)) = parse_asn_range(range_str) {
619 asn.push((AsnRange { start, end }, Arc::clone(&url_arc)));
620 }
621 }
622 }
623 }
624 }
625 }
626 }
627 }
628
629 Ok(BootstrapData {
630 dns,
631 ipv4,
632 ipv6,
633 asn,
634 })
635}
636
637fn build_rdap_url(base_url: &str, path: &str) -> String {
639 if base_url.ends_with('/') {
640 format!("{}{}", base_url, path)
641 } else {
642 format!("{}/{}", base_url, path)
643 }
644}
645
646fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
647 if let Some(pos) = range.find('-') {
648 let start = range[..pos].parse().ok()?;
649 let end = range[pos + 1..].parse().ok()?;
650 Some((start, end))
651 } else {
652 let num = range.parse().ok()?;
653 Some((num, num))
654 }
655}
656
657fn ipv4_matches_prefix(prefix: &str, ip: &Ipv4Addr) -> bool {
658 let (addr_part, mask_part) = match prefix.split_once('/') {
659 Some((a, m)) => (a, Some(m)),
660 None => (prefix, None),
661 };
662
663 let prefix_ip: Ipv4Addr = match addr_part.parse() {
664 Ok(ip) => ip,
665 Err(_) => return false,
666 };
667
668 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
669 Some(bits) if bits <= 32 => bits,
670 Some(_) => return false,
671 None => 32,
672 };
673
674 let mask = if mask_bits == 0 {
675 0
676 } else {
677 u32::MAX << (32 - mask_bits)
678 };
679
680 let ip_value = u32::from(*ip);
681 let prefix_value = u32::from(prefix_ip);
682
683 (ip_value & mask) == (prefix_value & mask)
684}
685
686fn ipv6_matches_prefix(prefix: &str, ip: &Ipv6Addr) -> bool {
687 let (addr_part, mask_part) = match prefix.split_once('/') {
688 Some((a, m)) => (a, Some(m)),
689 None => (prefix, None),
690 };
691
692 let prefix_ip: Ipv6Addr = match addr_part.parse() {
693 Ok(ip) => ip,
694 Err(_) => return false,
695 };
696
697 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
698 Some(bits) if bits <= 128 => bits,
699 Some(_) => return false,
700 None => 128,
701 };
702
703 let mask = if mask_bits == 0 {
704 0u128
705 } else {
706 u128::MAX << (128 - mask_bits)
707 };
708
709 let ip_value = ipv6_to_u128(ip);
710 let prefix_value = ipv6_to_u128(&prefix_ip);
711
712 (ip_value & mask) == (prefix_value & mask)
713}
714
715fn ipv6_to_u128(ip: &Ipv6Addr) -> u128 {
716 let segments = ip.segments();
717 let mut value = 0u128;
718 for segment in segments {
719 value = (value << 16) | segment as u128;
720 }
721 value
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[test]
729 fn test_default_client_has_retry_policy() {
730 let client = RdapClient::new();
731 assert_eq!(client.retry_policy.max_attempts, 2);
732 }
733
734 #[test]
735 fn test_client_without_retries() {
736 let client = RdapClient::new().without_retries();
737 assert_eq!(client.retry_policy.max_attempts, 1);
738 }
739
740 #[test]
741 fn test_client_custom_retry_policy() {
742 let policy = RetryPolicy::new().with_max_attempts(5);
743 let client = RdapClient::new().with_retry_policy(policy);
744 assert_eq!(client.retry_policy.max_attempts, 5);
745 }
746
747 #[test]
748 fn test_cached_bootstrap_expiration() {
749 let data = BootstrapData {
750 dns: HashMap::new(),
751 ipv4: Vec::new(),
752 ipv6: Vec::new(),
753 asn: Vec::new(),
754 };
755 let cached = CachedBootstrap::new(data);
756 assert!(!cached.is_expired());
758 }
759
760 #[test]
761 fn test_ipv4_prefix_matching_partial_mask() {
762 let ip_in = Ipv4Addr::new(203, 0, 114, 1);
763 let ip_out = Ipv4Addr::new(203, 0, 120, 1);
764 assert!(ipv4_matches_prefix("203.0.112.0/21", &ip_in));
765 assert!(!ipv4_matches_prefix("203.0.112.0/21", &ip_out));
766 }
767
768 #[test]
769 fn test_ipv6_prefix_matching_partial_mask() {
770 let ip_in: Ipv6Addr = "2001:db8::1".parse().unwrap();
771 let ip_out: Ipv6Addr = "2001:db9::1".parse().unwrap();
772 assert!(ipv6_matches_prefix("2001:db8::/33", &ip_in));
773 assert!(!ipv6_matches_prefix("2001:db8::/33", &ip_out));
774 }
775
776 #[test]
777 fn test_rdap_http_client_is_configured() {
778 let _client = &*RDAP_HTTP_CLIENT;
780 }
781
782 #[test]
783 fn test_parse_bootstrap_empty_services() {
784 let data = BootstrapData {
786 dns: HashMap::new(),
787 ipv4: Vec::new(),
788 ipv6: Vec::new(),
789 asn: Vec::new(),
790 };
791 assert!(RdapClient::get_rdap_url_for_domain(&data, "example.com").is_none());
793 assert!(RdapClient::get_rdap_url_for_asn(&data, 12345).is_none());
794 }
795}