Skip to main content

microsandbox_network/engine/dns/
nameserver.rs

1//! Upstream DNS resolution and host nameserver discovery.
2
3#[cfg(not(windows))]
4use std::net::IpAddr;
5use std::net::SocketAddr;
6#[cfg(not(windows))]
7use std::path::Path;
8
9#[cfg(not(windows))]
10use resolv_conf::Config as ResolvConfig;
11
12use crate::dns::Nameserver;
13
14#[cfg(target_os = "macos")]
15mod scdynamicstore;
16
17//--------------------------------------------------------------------------------------------------
18// Constants
19//--------------------------------------------------------------------------------------------------
20
21/// DNS port.
22#[cfg(not(windows))]
23const DNS_PORT: u16 = 53;
24
25/// Path to the host resolver configuration. Used as a fallback when explicit
26/// nameservers are not configured and the macOS dynamic store is unavailable.
27#[cfg(not(windows))]
28const RESOLV_CONF_PATH: &str = "/etc/resolv.conf";
29
30//--------------------------------------------------------------------------------------------------
31// Methods
32//--------------------------------------------------------------------------------------------------
33
34impl Nameserver {
35    /// Resolve to a concrete address through the host's resolver.
36    pub async fn resolve(&self) -> std::io::Result<SocketAddr> {
37        match self {
38            Self::Addr(address) => Ok(*address),
39            Self::Host { host, port } => tokio::net::lookup_host((host.as_str(), *port))
40                .await?
41                .next()
42                .ok_or_else(|| {
43                    std::io::Error::new(
44                        std::io::ErrorKind::NotFound,
45                        format!("no addresses resolved for {host}:{port}"),
46                    )
47                }),
48        }
49    }
50}
51
52//--------------------------------------------------------------------------------------------------
53// Functions
54//--------------------------------------------------------------------------------------------------
55
56/// Resolve configured nameservers to concrete addresses.
57pub(crate) async fn resolve_nameservers(
58    nameservers: &[Nameserver],
59) -> std::io::Result<Vec<SocketAddr>> {
60    let mut addresses = Vec::with_capacity(nameservers.len());
61    let mut last_error = None;
62    for nameserver in nameservers {
63        match nameserver.resolve().await {
64            Ok(address) => addresses.push(address),
65            Err(error) => {
66                tracing::warn!(nameserver = %nameserver, error = %error, "failed to resolve nameserver");
67                last_error = Some(error);
68            }
69        }
70    }
71    if addresses.is_empty()
72        && let Some(error) = last_error
73    {
74        return Err(error);
75    }
76    Ok(addresses)
77}
78
79/// Read the host's configured DNS servers.
80#[cfg(not(windows))]
81pub(crate) async fn read_host_dns_servers() -> std::io::Result<Vec<SocketAddr>> {
82    #[cfg(target_os = "macos")]
83    if let Some(servers) = try_read_scdynamicstore() {
84        return Ok(servers);
85    }
86    read_resolv_conf(Path::new(RESOLV_CONF_PATH)).await
87}
88
89#[cfg(target_os = "macos")]
90fn try_read_scdynamicstore() -> Option<Vec<SocketAddr>> {
91    match scdynamicstore::read_dns_servers() {
92        Ok(servers) if !servers.is_empty() => Some(servers),
93        Ok(_) => {
94            tracing::debug!(
95                "SCDynamicStore returned no nameservers; falling back to /etc/resolv.conf"
96            );
97            None
98        }
99        Err(error) => {
100            tracing::debug!(%error, "SCDynamicStore lookup failed; falling back to /etc/resolv.conf");
101            None
102        }
103    }
104}
105
106#[cfg(not(windows))]
107async fn read_resolv_conf(path: &Path) -> std::io::Result<Vec<SocketAddr>> {
108    let bytes = tokio::fs::read(path).await?;
109    let config = ResolvConfig::parse(&bytes)
110        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string()))?;
111    Ok(config
112        .nameservers
113        .into_iter()
114        .map(|nameserver| SocketAddr::new(IpAddr::from(nameserver), DNS_PORT))
115        .collect())
116}
117
118//--------------------------------------------------------------------------------------------------
119// Tests
120//--------------------------------------------------------------------------------------------------
121
122#[cfg(all(test, not(windows)))]
123mod tests {
124    use super::*;
125
126    #[tokio::test]
127    async fn read_resolv_conf_parses_nameservers() {
128        let path = std::env::temp_dir().join(format!("msb-resolv-{}.conf", std::process::id()));
129        std::fs::write(
130            &path,
131            "# comment line\n\
132             nameserver 1.1.1.1\n\
133             nameserver 8.8.8.8  # inline comment\n\
134             search example.com\n\
135             options ndots:5\n\
136             nameserver 2606:4700:4700::1111\n\
137             \n",
138        )
139        .unwrap();
140
141        let servers = read_resolv_conf(&path).await.expect("read ok");
142        std::fs::remove_file(&path).ok();
143
144        assert_eq!(servers.len(), 3);
145        assert_eq!(servers[0], "1.1.1.1:53".parse().unwrap());
146        assert_eq!(servers[1], "8.8.8.8:53".parse().unwrap());
147        assert_eq!(servers[2], "[2606:4700:4700::1111]:53".parse().unwrap());
148    }
149
150    #[tokio::test]
151    async fn read_resolv_conf_missing_file_errs() {
152        assert!(
153            read_resolv_conf(Path::new("/nonexistent/path/to/resolv.conf"))
154                .await
155                .is_err()
156        );
157    }
158}