1use hickory_proto::rr::domain::Name;
2use hickory_proto::rr::rdata::{A, AAAA};
3use hickory_proto::rr::{RData, Record, RecordType};
4#[cfg(not(target_arch = "wasm32"))]
5use hickory_resolver::config::{NameServerConfig, ResolverConfig};
6#[cfg(not(target_arch = "wasm32"))]
7use hickory_resolver::net::runtime::TokioRuntimeProvider;
8#[cfg(not(target_arch = "wasm32"))]
9use hickory_resolver::TokioResolver;
10use std::collections::{BTreeMap, BTreeSet};
11use std::error::Error;
12use std::fmt;
13use std::net::{IpAddr, SocketAddr};
14#[cfg(not(target_arch = "wasm32"))]
15use std::net::{Ipv4Addr, Ipv6Addr};
16use std::sync::Arc;
17#[cfg(not(target_arch = "wasm32"))]
18use std::sync::{mpsc, Mutex};
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct DnsConfig {
22 pub name_servers: Vec<SocketAddr>,
23 pub overrides: BTreeMap<String, Vec<IpAddr>>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum DnsLookupPolicy {
28 CheckPermissions,
29 SkipPermissions,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DnsLookupRequest {
34 hostname: String,
35 name_servers: Vec<SocketAddr>,
36}
37
38impl DnsLookupRequest {
39 pub fn new(hostname: impl Into<String>, name_servers: Vec<SocketAddr>) -> Self {
40 Self {
41 hostname: hostname.into(),
42 name_servers,
43 }
44 }
45
46 pub fn hostname(&self) -> &str {
47 &self.hostname
48 }
49
50 pub fn name_servers(&self) -> &[SocketAddr] {
51 &self.name_servers
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct DnsRecordLookupRequest {
57 hostname: String,
58 name_servers: Vec<SocketAddr>,
59 record_type: RecordType,
60}
61
62impl DnsRecordLookupRequest {
63 pub fn new(
64 hostname: impl Into<String>,
65 name_servers: Vec<SocketAddr>,
66 record_type: RecordType,
67 ) -> Self {
68 Self {
69 hostname: hostname.into(),
70 name_servers,
71 record_type,
72 }
73 }
74
75 pub fn hostname(&self) -> &str {
76 &self.hostname
77 }
78
79 pub fn name_servers(&self) -> &[SocketAddr] {
80 &self.name_servers
81 }
82
83 pub const fn record_type(&self) -> RecordType {
84 self.record_type
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum DnsResolutionSource {
90 Literal,
91 Override,
92 Resolver,
93}
94
95impl DnsResolutionSource {
96 pub const fn as_str(self) -> &'static str {
97 match self {
98 Self::Literal => "literal",
99 Self::Override => "override",
100 Self::Resolver => "resolver",
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct DnsResolution {
107 hostname: String,
108 source: DnsResolutionSource,
109 addresses: Vec<IpAddr>,
110}
111
112impl DnsResolution {
113 pub fn new(
114 hostname: impl Into<String>,
115 source: DnsResolutionSource,
116 addresses: Vec<IpAddr>,
117 ) -> Self {
118 Self {
119 hostname: hostname.into(),
120 source,
121 addresses,
122 }
123 }
124
125 pub fn hostname(&self) -> &str {
126 &self.hostname
127 }
128
129 pub const fn source(&self) -> DnsResolutionSource {
130 self.source
131 }
132
133 pub fn addresses(&self) -> &[IpAddr] {
134 &self.addresses
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct DnsRecordResolution {
140 hostname: String,
141 source: DnsResolutionSource,
142 records: Vec<Record>,
143}
144
145impl DnsRecordResolution {
146 pub fn new(
147 hostname: impl Into<String>,
148 source: DnsResolutionSource,
149 records: Vec<Record>,
150 ) -> Self {
151 Self {
152 hostname: hostname.into(),
153 source,
154 records,
155 }
156 }
157
158 pub fn hostname(&self) -> &str {
159 &self.hostname
160 }
161
162 pub const fn source(&self) -> DnsResolutionSource {
163 self.source
164 }
165
166 pub fn records(&self) -> &[Record] {
167 &self.records
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum DnsResolverErrorKind {
173 InvalidInput,
174 LookupFailed,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct DnsResolverError {
179 kind: DnsResolverErrorKind,
180 message: String,
181}
182
183impl DnsResolverError {
184 pub fn invalid_input(message: impl Into<String>) -> Self {
185 Self {
186 kind: DnsResolverErrorKind::InvalidInput,
187 message: message.into(),
188 }
189 }
190
191 pub fn lookup_failed(message: impl Into<String>) -> Self {
192 Self {
193 kind: DnsResolverErrorKind::LookupFailed,
194 message: message.into(),
195 }
196 }
197
198 pub const fn kind(&self) -> DnsResolverErrorKind {
199 self.kind
200 }
201}
202
203impl fmt::Display for DnsResolverError {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "{}", self.message)
206 }
207}
208
209impl Error for DnsResolverError {}
210
211pub trait DnsResolver {
212 fn lookup_ip(&self, request: &DnsLookupRequest) -> Result<Vec<IpAddr>, DnsResolverError>;
213 fn lookup_records(
214 &self,
215 request: &DnsRecordLookupRequest,
216 ) -> Result<Vec<Record>, DnsResolverError>;
217}
218
219pub type SharedDnsResolver = Arc<dyn DnsResolver + Send + Sync>;
220
221#[cfg(not(target_arch = "wasm32"))]
222pub struct HickoryDnsResolver {
223 worker: Mutex<mpsc::Sender<DnsWorkerRequest>>,
224}
225
226#[cfg(target_arch = "wasm32")]
230pub struct HickoryDnsResolver;
231
232#[cfg(not(target_arch = "wasm32"))]
233enum DnsWorkerRequest {
234 LookupIp {
235 hostname: String,
236 name_servers: Vec<SocketAddr>,
237 response: mpsc::Sender<Result<Vec<IpAddr>, DnsResolverError>>,
238 },
239 LookupRecords {
240 hostname: String,
241 name_servers: Vec<SocketAddr>,
242 record_type: RecordType,
243 response: mpsc::Sender<Result<Vec<Record>, DnsResolverError>>,
244 },
245}
246
247#[cfg(not(target_arch = "wasm32"))]
248impl Default for HickoryDnsResolver {
249 fn default() -> Self {
250 let (sender, receiver) = mpsc::channel();
251 std::thread::Builder::new()
252 .name("secure-exec-dns-resolver".to_owned())
253 .spawn(move || run_dns_worker(receiver))
254 .expect("failed to spawn DNS resolver worker");
255 Self {
256 worker: Mutex::new(sender),
257 }
258 }
259}
260
261#[cfg(target_arch = "wasm32")]
262impl Default for HickoryDnsResolver {
263 fn default() -> Self {
264 Self
265 }
266}
267
268#[cfg(not(target_arch = "wasm32"))]
269impl HickoryDnsResolver {
270 fn send_lookup_ip(
271 &self,
272 hostname: String,
273 name_servers: Vec<SocketAddr>,
274 ) -> Result<Vec<IpAddr>, DnsResolverError> {
275 let (response, receiver) = mpsc::channel();
276 self.worker
277 .lock()
278 .unwrap_or_else(|poisoned| poisoned.into_inner())
279 .send(DnsWorkerRequest::LookupIp {
280 hostname,
281 name_servers,
282 response,
283 })
284 .map_err(|_| DnsResolverError::lookup_failed("dns resolver worker stopped"))?;
285 receiver
286 .recv()
287 .map_err(|_| DnsResolverError::lookup_failed("dns resolver worker stopped"))?
288 }
289
290 fn send_lookup_records(
291 &self,
292 hostname: String,
293 name_servers: Vec<SocketAddr>,
294 record_type: RecordType,
295 ) -> Result<Vec<Record>, DnsResolverError> {
296 let (response, receiver) = mpsc::channel();
297 self.worker
298 .lock()
299 .unwrap_or_else(|poisoned| poisoned.into_inner())
300 .send(DnsWorkerRequest::LookupRecords {
301 hostname,
302 name_servers,
303 record_type,
304 response,
305 })
306 .map_err(|_| DnsResolverError::lookup_failed("dns resolver worker stopped"))?;
307 receiver
308 .recv()
309 .map_err(|_| DnsResolverError::lookup_failed("dns resolver worker stopped"))?
310 }
311}
312
313#[cfg(not(target_arch = "wasm32"))]
314impl DnsResolver for HickoryDnsResolver {
315 fn lookup_ip(&self, request: &DnsLookupRequest) -> Result<Vec<IpAddr>, DnsResolverError> {
316 self.send_lookup_ip(
317 request.hostname().to_owned(),
318 request.name_servers().to_vec(),
319 )
320 }
321
322 fn lookup_records(
323 &self,
324 request: &DnsRecordLookupRequest,
325 ) -> Result<Vec<Record>, DnsResolverError> {
326 self.send_lookup_records(
327 request.hostname().to_owned(),
328 request.name_servers().to_vec(),
329 request.record_type(),
330 )
331 }
332}
333
334#[cfg(not(target_arch = "wasm32"))]
335fn run_dns_worker(receiver: mpsc::Receiver<DnsWorkerRequest>) {
336 let runtime = match tokio::runtime::Runtime::new() {
337 Ok(runtime) => runtime,
338 Err(error) => {
339 for request in receiver {
340 request.respond_with_error(DnsResolverError::lookup_failed(format!(
341 "failed to create DNS runtime: {error}"
342 )));
343 }
344 return;
345 }
346 };
347 let mut resolvers = BTreeMap::new();
348 for request in receiver {
349 match request {
350 DnsWorkerRequest::LookupIp {
351 hostname,
352 name_servers,
353 response,
354 } => {
355 let result = worker_lookup_ip(&runtime, &mut resolvers, hostname, name_servers);
356 let _ = response.send(result);
357 }
358 DnsWorkerRequest::LookupRecords {
359 hostname,
360 name_servers,
361 record_type,
362 response,
363 } => {
364 let result = worker_lookup_records(
365 &runtime,
366 &mut resolvers,
367 hostname,
368 name_servers,
369 record_type,
370 );
371 let _ = response.send(result);
372 }
373 }
374 }
375}
376
377#[cfg(not(target_arch = "wasm32"))]
378impl DnsWorkerRequest {
379 fn respond_with_error(self, error: DnsResolverError) {
380 match self {
381 Self::LookupIp { response, .. } => {
382 let _ = response.send(Err(error));
383 }
384 Self::LookupRecords { response, .. } => {
385 let _ = response.send(Err(error));
386 }
387 }
388 }
389}
390
391#[cfg(not(target_arch = "wasm32"))]
392fn worker_resolver_for(
393 resolvers: &mut BTreeMap<Vec<SocketAddr>, TokioResolver>,
394 name_servers: &[SocketAddr],
395) -> Result<TokioResolver, DnsResolverError> {
396 let key = name_servers.to_vec();
397 if let Some(resolver) = resolvers.get(&key).cloned() {
398 return Ok(resolver);
399 }
400
401 let resolver_config = resolver_config_from_name_servers(name_servers);
402 let builder = if let Some(config) = resolver_config {
403 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
404 } else {
405 TokioResolver::builder_tokio().map_err(|error| {
406 DnsResolverError::lookup_failed(format!(
407 "failed to initialize DNS resolver from system configuration: {error}"
408 ))
409 })?
410 };
411 let resolver = builder.build().map_err(|error| {
412 DnsResolverError::lookup_failed(format!("failed to build DNS resolver: {error}"))
413 })?;
414 let resolver = resolvers.entry(key).or_insert_with(|| resolver).clone();
415 Ok(resolver)
416}
417
418#[cfg(not(target_arch = "wasm32"))]
419fn worker_lookup_ip(
420 runtime: &tokio::runtime::Runtime,
421 resolvers: &mut BTreeMap<Vec<SocketAddr>, TokioResolver>,
422 hostname: String,
423 name_servers: Vec<SocketAddr>,
424) -> Result<Vec<IpAddr>, DnsResolverError> {
425 let resolver = worker_resolver_for(resolvers, &name_servers)?;
426 runtime.block_on(async move {
427 let lookup = resolver.lookup_ip(&hostname).await.map_err(|error| {
428 DnsResolverError::lookup_failed(format!(
429 "failed to resolve DNS address {hostname}: {error}"
430 ))
431 })?;
432
433 let mut addresses = Vec::new();
434 let mut seen = BTreeSet::new();
435 for ip in lookup.iter() {
436 if seen.insert(ip) {
437 addresses.push(ip);
438 }
439 }
440
441 if addresses.is_empty() {
442 return Err(DnsResolverError::lookup_failed(format!(
443 "failed to resolve DNS address {hostname}"
444 )));
445 }
446
447 Ok(addresses)
448 })
449}
450
451#[cfg(not(target_arch = "wasm32"))]
452fn worker_lookup_records(
453 runtime: &tokio::runtime::Runtime,
454 resolvers: &mut BTreeMap<Vec<SocketAddr>, TokioResolver>,
455 hostname: String,
456 name_servers: Vec<SocketAddr>,
457 record_type: RecordType,
458) -> Result<Vec<Record>, DnsResolverError> {
459 let resolver = worker_resolver_for(resolvers, &name_servers)?;
460 runtime.block_on(async move {
461 let lookup = resolver
462 .lookup(&hostname, record_type)
463 .await
464 .map_err(|error| {
465 DnsResolverError::lookup_failed(format!(
466 "failed to resolve DNS {record_type} record {hostname}: {error}"
467 ))
468 })?;
469 let records = lookup.answers().to_vec();
470 if records.is_empty() {
471 return Err(DnsResolverError::lookup_failed(format!(
472 "failed to resolve DNS {record_type} record {hostname}"
473 )));
474 }
475 Ok(records)
476 })
477}
478
479#[cfg(target_arch = "wasm32")]
480impl DnsResolver for HickoryDnsResolver {
481 fn lookup_ip(&self, request: &DnsLookupRequest) -> Result<Vec<IpAddr>, DnsResolverError> {
482 Err(DnsResolverError::lookup_failed(format!(
483 "browser sidecar DNS resolver is unavailable for {}; configure DNS overrides or pass a literal address",
484 request.hostname()
485 )))
486 }
487
488 fn lookup_records(
489 &self,
490 request: &DnsRecordLookupRequest,
491 ) -> Result<Vec<Record>, DnsResolverError> {
492 Err(DnsResolverError::lookup_failed(format!(
493 "browser sidecar DNS record resolver is unavailable for {}; configure DNS overrides or pass a literal address",
494 request.hostname()
495 )))
496 }
497}
498
499pub fn normalize_dns_hostname(hostname: &str) -> Result<String, DnsResolverError> {
500 let normalized = hostname.trim().trim_end_matches('.').to_ascii_lowercase();
501 if normalized.is_empty() {
502 return Err(DnsResolverError::invalid_input(
503 "DNS hostname must not be empty",
504 ));
505 }
506 Ok(normalized)
507}
508
509pub fn format_dns_resource(hostname: &str) -> Result<String, DnsResolverError> {
510 Ok(format!("dns://{}", canonical_dns_subject(hostname)?))
511}
512
513pub fn resolve_dns(
514 config: &DnsConfig,
515 resolver: &dyn DnsResolver,
516 hostname: &str,
517) -> Result<DnsResolution, DnsResolverError> {
518 let trimmed = hostname.trim();
519 if let Ok(ip_addr) = trimmed.parse::<IpAddr>() {
520 return Ok(DnsResolution::new(
521 ip_addr.to_string(),
522 DnsResolutionSource::Literal,
523 vec![ip_addr],
524 ));
525 }
526
527 let normalized_hostname = normalize_dns_hostname(trimmed)?;
528 if let Some(addresses) = config.overrides.get(&normalized_hostname) {
529 return Ok(DnsResolution::new(
530 normalized_hostname,
531 DnsResolutionSource::Override,
532 addresses.clone(),
533 ));
534 }
535
536 let request = DnsLookupRequest::new(normalized_hostname.clone(), config.name_servers.clone());
537 let addresses = resolver.lookup_ip(&request)?;
538 if addresses.is_empty() {
539 return Err(DnsResolverError::lookup_failed(format!(
540 "failed to resolve DNS address {normalized_hostname}"
541 )));
542 }
543
544 Ok(DnsResolution::new(
545 normalized_hostname,
546 DnsResolutionSource::Resolver,
547 dedupe_addresses(addresses),
548 ))
549}
550
551pub fn resolve_dns_records(
552 config: &DnsConfig,
553 resolver: &dyn DnsResolver,
554 hostname: &str,
555 record_type: RecordType,
556) -> Result<DnsRecordResolution, DnsResolverError> {
557 let trimmed = hostname.trim();
558 let normalized_hostname = normalize_dns_hostname(trimmed)?;
559 let owner_name = normalized_hostname.parse::<Name>().map_err(|error| {
560 DnsResolverError::invalid_input(format!("invalid DNS hostname: {error}"))
561 })?;
562
563 if let Some(records) = records_from_literal(trimmed, owner_name.clone(), record_type) {
564 return Ok(DnsRecordResolution::new(
565 normalized_hostname,
566 DnsResolutionSource::Literal,
567 records,
568 ));
569 }
570
571 if let Some(addresses) = config.overrides.get(&normalized_hostname) {
572 let records = records_from_addresses(owner_name.clone(), addresses, record_type);
573 if !records.is_empty() {
574 return Ok(DnsRecordResolution::new(
575 normalized_hostname,
576 DnsResolutionSource::Override,
577 records,
578 ));
579 }
580 }
581
582 let request = DnsRecordLookupRequest::new(
583 normalized_hostname.clone(),
584 config.name_servers.clone(),
585 record_type,
586 );
587 let records = resolver.lookup_records(&request)?;
588 if records.is_empty() {
589 return Err(DnsResolverError::lookup_failed(format!(
590 "failed to resolve DNS {record_type} record {normalized_hostname}"
591 )));
592 }
593
594 Ok(DnsRecordResolution::new(
595 normalized_hostname,
596 DnsResolutionSource::Resolver,
597 records,
598 ))
599}
600
601fn canonical_dns_subject(hostname: &str) -> Result<String, DnsResolverError> {
602 let trimmed = hostname.trim();
603 if let Ok(ip_addr) = trimmed.parse::<IpAddr>() {
604 return Ok(ip_addr.to_string());
605 }
606
607 normalize_dns_hostname(trimmed)
608}
609
610#[cfg(not(target_arch = "wasm32"))]
611fn resolver_config_from_name_servers(name_servers: &[SocketAddr]) -> Option<ResolverConfig> {
612 if name_servers.is_empty() {
613 return None;
614 }
615
616 let name_servers = name_servers
617 .iter()
618 .map(|server| {
619 let mut config = NameServerConfig::udp_and_tcp(server.ip());
620 for connection in &mut config.connections {
621 connection.port = server.port();
622 connection.bind_addr = Some(SocketAddr::new(
623 if server.is_ipv6() {
624 IpAddr::V6(Ipv6Addr::UNSPECIFIED)
625 } else {
626 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
627 },
628 0,
629 ));
630 }
631 config
632 })
633 .collect();
634
635 Some(ResolverConfig::from_parts(None, vec![], name_servers))
636}
637
638fn dedupe_addresses(addresses: Vec<IpAddr>) -> Vec<IpAddr> {
639 let mut deduped = Vec::with_capacity(addresses.len());
640 let mut seen = BTreeSet::new();
641 for address in addresses {
642 if seen.insert(address) {
643 deduped.push(address);
644 }
645 }
646 deduped
647}
648
649fn records_from_literal(
650 hostname: &str,
651 owner_name: Name,
652 record_type: RecordType,
653) -> Option<Vec<Record>> {
654 let ip_addr = hostname.parse::<IpAddr>().ok()?;
655 let records = records_from_addresses(owner_name, &[ip_addr], record_type);
656 if records.is_empty() {
657 return None;
658 }
659 Some(records)
660}
661
662fn records_from_addresses(
663 owner_name: Name,
664 addresses: &[IpAddr],
665 record_type: RecordType,
666) -> Vec<Record> {
667 addresses
668 .iter()
669 .filter_map(|ip| match (record_type, ip) {
670 (RecordType::A, IpAddr::V4(ipv4)) | (RecordType::ANY, IpAddr::V4(ipv4)) => Some(
671 Record::from_rdata(owner_name.clone(), 60, RData::A(A::from(*ipv4))),
672 ),
673 (RecordType::AAAA, IpAddr::V6(ipv6)) | (RecordType::ANY, IpAddr::V6(ipv6)) => Some(
674 Record::from_rdata(owner_name.clone(), 60, RData::AAAA(AAAA::from(*ipv6))),
675 ),
676 _ => None,
677 })
678 .collect()
679}