1use crate::core::MtopError;
2use std::fmt::Debug;
3use std::net::{IpAddr, SocketAddr};
4use std::str::FromStr;
5use std::time::Duration;
6use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
7
8const DEFAULT_PORT: u16 = 53;
9const MAX_NAMESERVERS: usize = 3;
10
11#[derive(Debug, Default, Clone, Eq, PartialEq)]
15pub struct ResolvConf {
16 pub nameservers: Vec<SocketAddr>,
17 pub options: ResolvConfOptions,
18}
19
20#[derive(Debug, Default, Clone, Eq, PartialEq)]
24pub struct ResolvConfOptions {
25 pub timeout: Option<Duration>,
26 pub attempts: Option<u8>,
27 pub rotate: Option<bool>,
28}
29
30pub async fn config<R>(read: R) -> Result<ResolvConf, MtopError>
32where
33 R: AsyncRead + Send + Sync + Unpin + 'static,
34{
35 let mut lines = BufReader::new(read).lines();
36 let mut conf = ResolvConf::default();
37
38 while let Some(line) = lines.next_line().await? {
39 let line = line.trim();
40 if line.is_empty() || line.starts_with('#') {
41 continue;
42 }
43
44 let mut parts = line.split_whitespace();
45 let Some(key) = parts.next() else {
46 tracing::debug!(message = "skipping malformed resolv.conf line", line = line);
47 continue;
48 };
49
50 match Token::get(key) {
51 Some(Token::NameServer) => {
52 if conf.nameservers.len() < MAX_NAMESERVERS {
53 conf.nameservers.push(parse_nameserver(line, parts)?);
54 }
55 }
56 Some(Token::Options) => {
57 for opt in parse_options(parts) {
58 match opt {
59 OptionsToken::Timeout(t) => {
60 conf.options.timeout = Some(Duration::from_secs(u64::from(t)));
61 }
62 OptionsToken::Attempts(n) => {
63 conf.options.attempts = Some(n);
64 }
65 OptionsToken::Rotate => {
66 conf.options.rotate = Some(true);
67 }
68 }
69 }
70 }
71 None => {
72 tracing::debug!(
73 message = "skipping unknown resolv.conf setting",
74 setting = key,
75 line = line
76 );
77 }
78 }
79 }
80
81 Ok(conf)
82}
83
84fn parse_nameserver<'a>(line: &str, mut parts: impl Iterator<Item = &'a str>) -> Result<SocketAddr, MtopError> {
87 if let Some(part) = parts.next() {
88 part.parse::<IpAddr>()
89 .map(|ip| (ip, DEFAULT_PORT).into())
90 .map_err(|e| MtopError::configuration_cause(format!("malformed nameserver address '{}'", part), e))
91 } else {
92 Err(MtopError::configuration(format!(
93 "malformed nameserver configuration '{}'",
94 line
95 )))
96 }
97}
98
99fn parse_options<'a>(parts: impl Iterator<Item = &'a str>) -> Vec<OptionsToken> {
102 let mut out = Vec::new();
103
104 for part in parts {
105 let opt = match part.parse() {
106 Ok(o) => o,
107 Err(e) => {
108 tracing::debug!(message = "skipping unknown resolv.conf option", option = part, err = %e);
109 continue;
110 }
111 };
112
113 out.push(opt);
114 }
115 out
116}
117
118#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
122enum Token {
123 NameServer,
124 Options,
125}
126
127impl Token {
128 fn get(s: &str) -> Option<Self> {
129 match s {
130 "nameserver" => Some(Self::NameServer),
131 "options" => Some(Self::Options),
132 _ => None,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
141enum OptionsToken {
142 Timeout(u8),
143 Attempts(u8),
144 Rotate,
145}
146
147impl OptionsToken {
148 const MAX_TIMEOUT: u8 = 30;
149 const MAX_ATTEMPTS: u8 = 5;
150
151 fn parse(line: &str, val: &str, max: u8) -> Result<u8, MtopError> {
152 let n: u8 = val
153 .parse()
154 .map_err(|e| MtopError::configuration_cause(format!("unable to parse {} value '{}'", line, val), e))?;
155
156 Ok(n.min(max))
157 }
158}
159
160impl FromStr for OptionsToken {
161 type Err = MtopError;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 if s == "rotate" {
165 Ok(Self::Rotate)
166 } else {
167 match s.split_once(':') {
168 Some(("timeout", v)) => Ok(Self::Timeout(Self::parse(s, v, Self::MAX_TIMEOUT)?)),
169 Some(("attempts", v)) => Ok(Self::Attempts(Self::parse(s, v, Self::MAX_ATTEMPTS)?)),
170 _ => Err(MtopError::configuration(format!("unknown option {}", s))),
171 }
172 }
173 }
174}
175
176#[cfg(test)]
177mod test {
178 use super::{OptionsToken, Token, config};
179 use crate::core::ErrorKind;
180 use crate::dns::{ResolvConf, ResolvConfOptions};
181 use std::io::{Cursor, Error as IOError, ErrorKind as IOErrorKind};
182 use std::pin::Pin;
183 use std::str::FromStr;
184 use std::task::{Context, Poll};
185 use std::time::Duration;
186 use tokio::io::{AsyncRead, ReadBuf};
187
188 #[test]
189 fn test_configuration() {
190 assert_eq!(Some(Token::NameServer), Token::get("nameserver"));
191 assert_eq!(Some(Token::Options), Token::get("options"));
192 assert_eq!(None, Token::get("invalid"));
193 }
194
195 #[test]
196 fn test_configuration_option_success() {
197 assert_eq!(OptionsToken::Rotate, OptionsToken::from_str("rotate").unwrap());
198 assert_eq!(OptionsToken::Timeout(3), OptionsToken::from_str("timeout:3").unwrap());
199 assert_eq!(OptionsToken::Attempts(4), OptionsToken::from_str("attempts:4").unwrap());
200 }
201
202 #[test]
203 fn test_configuration_option_limits() {
204 assert_eq!(OptionsToken::Timeout(30), OptionsToken::from_str("timeout:35").unwrap());
205 assert_eq!(
206 OptionsToken::Attempts(5),
207 OptionsToken::from_str("attempts:10").unwrap()
208 );
209 }
210
211 #[test]
212 fn test_configuration_option_error() {
213 assert!(OptionsToken::from_str("ndots:bad").is_err());
214 assert!(OptionsToken::from_str("timeout:bad").is_err());
215 assert!(OptionsToken::from_str("attempts:-5").is_err());
216 }
217
218 #[tokio::test]
219 async fn test_config_read_error() {
220 struct ErrAsyncRead;
221 impl AsyncRead for ErrAsyncRead {
222 fn poll_read(
223 self: Pin<&mut Self>,
224 _cx: &mut Context<'_>,
225 _buf: &mut ReadBuf<'_>,
226 ) -> Poll<std::io::Result<()>> {
227 Poll::Ready(Err(IOError::new(IOErrorKind::UnexpectedEof, "test error")))
228 }
229 }
230
231 let reader = ErrAsyncRead;
232 let res = config(reader).await.unwrap_err();
233 assert_eq!(ErrorKind::IO, res.kind());
234 }
235
236 #[tokio::test]
237 async fn test_config_no_content() {
238 let reader = Cursor::new(Vec::new());
239 let res = config(reader).await.unwrap();
240 assert_eq!(ResolvConf::default(), res);
241 }
242
243 #[tokio::test]
244 async fn test_config_all_comments() {
245 #[rustfmt::skip]
246 let reader = Cursor::new(concat!(
247 "# this is a comment\n",
248 "# another comment\n",
249 ));
250 let res = config(reader).await.unwrap();
251 assert_eq!(ResolvConf::default(), res);
252 }
253
254 #[tokio::test]
255 async fn test_config_all_unsupported() {
256 #[rustfmt::skip]
257 let reader = Cursor::new(concat!(
258 "scrambler 127.0.0.5\n",
259 "invalid directive\n",
260 ));
261 let res = config(reader).await.unwrap();
262 assert_eq!(ResolvConf::default(), res);
263 }
264
265 #[tokio::test]
266 async fn test_config_nameservers_search_invalid_options() {
267 #[rustfmt::skip]
268 let reader = Cursor::new(concat!(
269 "# this is a comment\n",
270 "nameserver 127.0.0.53\n",
271 "options casual-fridays:true\n",
272 ));
273
274 let expected = ResolvConf {
275 nameservers: vec!["127.0.0.53:53".parse().unwrap()],
276 options: ResolvConfOptions::default(),
277 };
278
279 let res = config(reader).await.unwrap();
280 assert_eq!(expected, res);
281 }
282
283 #[tokio::test]
284 async fn test_config_nameservers_search_no_options() {
285 #[rustfmt::skip]
286 let reader = Cursor::new(concat!(
287 "# this is a comment\n",
288 "nameserver 127.0.0.53\n",
289 ));
290
291 let expected = ResolvConf {
292 nameservers: vec!["127.0.0.53:53".parse().unwrap()],
293 options: ResolvConfOptions::default(),
294 };
295
296 let res = config(reader).await.unwrap();
297 assert_eq!(expected, res);
298 }
299
300 #[tokio::test]
301 async fn test_config_nameservers_search_options() {
302 #[rustfmt::skip]
303 let reader = Cursor::new(concat!(
304 "# this is a comment\n",
305 "nameserver 127.0.0.53\n",
306 "nameserver 127.0.0.54\n",
307 "nameserver 127.0.0.55\n",
308 "options ndots:3 attempts:5 timeout:10 rotate use-vc edns0\n",
309 ));
310
311 let expected = ResolvConf {
312 nameservers: vec![
313 "127.0.0.53:53".parse().unwrap(),
314 "127.0.0.54:53".parse().unwrap(),
315 "127.0.0.55:53".parse().unwrap(),
316 ],
317 options: ResolvConfOptions {
318 timeout: Some(Duration::from_secs(10)),
319 attempts: Some(5),
320 rotate: Some(true),
321 },
322 };
323
324 let res = config(reader).await.unwrap();
325 assert_eq!(expected, res);
326 }
327}