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