Skip to main content

agentos_kernel/
dns.rs

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/// On wasm the kernel has no tokio runtime or host DNS stack, so the resolver is
227/// a unit type whose `DnsResolver` impl reports that name resolution is
228/// unavailable; guests must supply DNS overrides or literal addresses.
229#[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}