microsandbox_network/engine/dns/
nameserver.rs1#[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#[cfg(not(windows))]
23const DNS_PORT: u16 = 53;
24
25#[cfg(not(windows))]
28const RESOLV_CONF_PATH: &str = "/etc/resolv.conf";
29
30impl Nameserver {
35 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
52pub(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#[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#[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}