nmstate/
dns.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::net::{Ipv4Addr, Ipv6Addr};
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    ip::is_ipv6_addr, ErrorKind, MergedInterface, MergedNetworkState,
10    NmstateError,
11};
12
13const SUPPORTED_DNS_OPTS_NO_VALUE: [&str; 15] = [
14    "debug",
15    "edns0",
16    "inet6",
17    "ip6-bytestring",
18    "ip6-dotint",
19    "no-aaaa",
20    "no-check-names",
21    "no-ip6-dotint",
22    "no-reload",
23    "no-tld-query",
24    "rotate",
25    "single-request",
26    "single-request-reopen",
27    "trust-ad",
28    "use-vc",
29];
30
31const SUPPORTED_DNS_OPTS_WITH_VALUE: [&str; 3] =
32    ["ndots", "timeout", "attempts"];
33
34#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
35#[non_exhaustive]
36#[serde(deny_unknown_fields)]
37/// DNS resolver state. Example partial yaml output of [NetworkState] with
38/// static DNS config:
39/// ```yaml
40/// ---
41/// dns-resolver:
42///   running:
43///      server:
44///      - 2001:db8:1::250
45///      - 192.0.2.250
46///      search:
47///      - example.org
48///      - example.net
49///   config:
50///      search:
51///      - example.org
52///      - example.net
53///      server:
54///      - 2001:db8:1::250
55///      - 192.0.2.250
56///      options:
57///      - trust-ad
58///      - rotate
59/// ```
60/// To purge all static DNS configuration:
61/// ```yml
62/// ---
63/// dns-resolver:
64///   config: {}
65/// ```
66pub struct DnsState {
67    #[serde(skip_serializing_if = "Option::is_none")]
68    /// The running effective state. The DNS server might be from DHCP(IPv6
69    /// autoconf) or manual setup.
70    /// Ignored when applying state.
71    pub running: Option<DnsClientState>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    /// The static saved DNS resolver config.
74    /// When applying, if this not mentioned(None), current static DNS config
75    /// will be preserved as it was. If defined(Some), will override current
76    /// static DNS config.
77    pub config: Option<DnsClientState>,
78}
79
80impl DnsState {
81    /// [DnsState] with empty static DNS resolver config.
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    pub fn is_empty(&self) -> bool {
87        self.running.is_none() && self.config.is_none()
88    }
89
90    pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
91        if let Some(config) = self.config.as_mut() {
92            config.sanitize()?;
93        }
94        Ok(())
95    }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
99#[non_exhaustive]
100#[serde(deny_unknown_fields)]
101/// DNS Client state
102pub struct DnsClientState {
103    #[serde(skip_serializing_if = "Option::is_none")]
104    /// Name server IP address list.
105    /// To remove all existing servers, please use `Some(Vec::new())`.
106    /// If undefined(set to `None`), will preserve current config.
107    pub server: Option<Vec<String>>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    /// Search list for host-name lookup.
110    /// To remove all existing search, please use `Some(Vec::new())`.
111    /// If undefined(set to `None`), will preserve current config.
112    pub search: Option<Vec<String>>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    /// DNS option list.
115    /// To remove all existing search, please use `Some(Vec::new())`.
116    /// If undefined(set to `None`), will preserve current config.
117    pub options: Option<Vec<String>>,
118    #[serde(skip)]
119    // Lower is better
120    pub(crate) priority: Option<i32>,
121}
122
123impl DnsClientState {
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    pub fn is_empty(&self) -> bool {
129        self.server.is_none() && self.search.is_none() && self.options.is_none()
130    }
131
132    pub(crate) fn is_null(&self) -> bool {
133        self.server.as_ref().map(|s| s.len()).unwrap_or_default() == 0
134            && self.search.as_ref().map(|s| s.len()).unwrap_or_default() == 0
135            && self.options.as_ref().map(|s| s.len()).unwrap_or_default() == 0
136    }
137
138    // sanitize the IP addresses.
139    pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
140        if let Some(srvs) = self.server.as_mut() {
141            let mut sanitized_srvs = Vec::new();
142            for srv in srvs {
143                if is_ipv6_addr(srv.as_str()) {
144                    let splits: Vec<&str> = srv.split('%').collect();
145                    if splits.len() == 2 {
146                        if let Ok(ip_addr) = splits[0].parse::<Ipv6Addr>() {
147                            sanitized_srvs
148                                .push(format!("{}%{}", ip_addr, splits[1]));
149                        }
150                    } else if let Ok(ip_addr) = srv.parse::<Ipv6Addr>() {
151                        sanitized_srvs.push(ip_addr.to_string());
152                    } else {
153                        return Err(NmstateError::new(
154                            ErrorKind::InvalidArgument,
155                            format!("Invalid DNS server string {srv}",),
156                        ));
157                    }
158                } else if let Ok(ip_addr) = srv.parse::<Ipv4Addr>() {
159                    sanitized_srvs.push(ip_addr.to_string());
160                } else {
161                    return Err(NmstateError::new(
162                        ErrorKind::InvalidArgument,
163                        format!("Invalid DNS server string {srv}",),
164                    ));
165                }
166            }
167            self.server = Some(sanitized_srvs);
168        }
169        if let Some(opts) = self.options.as_ref() {
170            for opt in opts {
171                match opt.find(':') {
172                    Some(i) => {
173                        let opt = &opt[..i];
174                        if !SUPPORTED_DNS_OPTS_WITH_VALUE.contains(&opt) {
175                            return Err(NmstateError::new(
176                                ErrorKind::InvalidArgument,
177                                format!(
178                                    "Option '{opt}' is not supported to hold \
179                                     a value, only support these without \
180                                     value: {} and these with values: {}:n",
181                                    SUPPORTED_DNS_OPTS_NO_VALUE.join(", "),
182                                    SUPPORTED_DNS_OPTS_WITH_VALUE.join(":n, ")
183                                ),
184                            ));
185                        }
186                    }
187                    None => {
188                        if !SUPPORTED_DNS_OPTS_NO_VALUE.contains(&opt.as_str())
189                        {
190                            return Err(NmstateError::new(
191                                ErrorKind::InvalidArgument,
192                                format!(
193                                    "Unsupported DNS option {opt}, only \
194                                     support these without value: {} and \
195                                     these with values: {}",
196                                    SUPPORTED_DNS_OPTS_NO_VALUE.join(", "),
197                                    SUPPORTED_DNS_OPTS_WITH_VALUE.join(":n, ")
198                                ),
199                            ));
200                        }
201                    }
202                }
203            }
204        }
205        Ok(())
206    }
207}
208
209#[derive(Clone, Debug, Default, PartialEq, Eq)]
210pub(crate) struct MergedDnsState {
211    pub(crate) desired: Option<DnsState>,
212    pub(crate) current: DnsState,
213    pub(crate) servers: Vec<String>,
214    pub(crate) searches: Vec<String>,
215    pub(crate) options: Vec<String>,
216}
217
218impl MergedDnsState {
219    pub(crate) fn new(
220        desired: Option<DnsState>,
221        mut current: DnsState,
222    ) -> Result<Self, NmstateError> {
223        current.sanitize().ok();
224        let mut servers = current
225            .config
226            .as_ref()
227            .and_then(|c| c.server.clone())
228            .unwrap_or_default();
229        let mut searches = current
230            .config
231            .as_ref()
232            .and_then(|c| c.search.clone())
233            .unwrap_or_default();
234
235        let mut options = current
236            .config
237            .as_ref()
238            .and_then(|c| c.options.clone())
239            .unwrap_or_default();
240
241        let mut desired = match desired {
242            Some(d) => d,
243            None => {
244                return Ok(Self {
245                    desired: None,
246                    current,
247                    servers,
248                    searches,
249                    options,
250                });
251            }
252        };
253
254        desired.sanitize()?;
255
256        if let Some(conf) = desired.config.as_ref() {
257            //  * `server`, `search` and `options` are None. Equal to desire
258            //  state `config: {}`, means purging
259            if conf.server.is_none()
260                && conf.search.is_none()
261                && conf.options.is_none()
262            {
263                servers.clear();
264                searches.clear();
265                options.clear();
266            } else {
267                if let Some(des_srvs) = conf.server.as_ref() {
268                    servers.clear();
269                    servers.extend_from_slice(des_srvs);
270                }
271                if let Some(des_schs) = conf.search.as_ref() {
272                    searches.clear();
273                    searches.extend_from_slice(des_schs);
274                }
275                if let Some(des_opts) = conf.options.as_ref() {
276                    options.clear();
277                    options.extend_from_slice(des_opts);
278                }
279            }
280        }
281
282        Ok(Self {
283            desired: Some(desired),
284            current,
285            servers,
286            searches,
287            options,
288        })
289    }
290
291    pub(crate) fn is_search_or_option_only(&self) -> bool {
292        self.servers.is_empty()
293            && (!self.searches.is_empty() || !self.options.is_empty())
294    }
295}
296
297impl MergedNetworkState {
298    // * Specified interface is valid for hold IPv6 DNS config.
299    // * Cannot have more than one IPv6 link-local DNS interface.
300    pub(crate) fn validate_ipv6_link_local_address_dns_srv(
301        &self,
302    ) -> Result<(), NmstateError> {
303        let mut iface_names = Vec::new();
304        for srv in self.dns.servers.as_slice() {
305            if let Some((_, iface_name)) = parse_dns_ipv6_link_local_srv(srv)? {
306                let iface = if let Some(iface) =
307                    self.interfaces.kernel_ifaces.get(iface_name)
308                {
309                    iface
310                } else {
311                    return Err(NmstateError::new(
312                        ErrorKind::InvalidArgument,
313                        format!(
314                            "Desired IPv6 link local DNS server {srv} is \
315                             pointing to interface {iface_name} which does \
316                             not exist."
317                        ),
318                    ));
319                };
320                if iface.is_iface_valid_for_dns(true) {
321                    iface_names.push(iface.merged.name());
322                } else {
323                    return Err(NmstateError::new(
324                        ErrorKind::InvalidArgument,
325                        format!(
326                            "Interface {iface_name} has IPv6 disabled, hence \
327                             cannot hold desired IPv6 link local DNS server \
328                             {srv}"
329                        ),
330                    ));
331                }
332            }
333        }
334        if iface_names.len() >= 2 {
335            return Err(NmstateError::new(
336                ErrorKind::NotImplementedError,
337                format!(
338                    "Only support IPv6 link local DNS name server(s) pointing \
339                     to a single interface, but got '{}'",
340                    iface_names.join(" ")
341                ),
342            ));
343        }
344
345        Ok(())
346    }
347}
348
349pub(crate) fn parse_dns_ipv6_link_local_srv(
350    srv: &str,
351) -> Result<Option<(std::net::Ipv6Addr, &str)>, NmstateError> {
352    if srv.contains('%') {
353        let splits: Vec<&str> = srv.split('%').collect();
354        if splits.len() == 2 {
355            match std::net::Ipv6Addr::from_str(splits[0]) {
356                Ok(ip) => return Ok(Some((ip, splits[1]))),
357                Err(_) => {
358                    return Err(NmstateError::new(
359                        ErrorKind::InvalidArgument,
360                        format!(
361                            "Invalid IPv6 address in {srv}, only IPv6 link \
362                             local address is allowed to have '%' character \
363                             in DNS name server, the correct format should be \
364                             'fe80::deef:1%eth1'"
365                        ),
366                    ));
367                }
368            }
369        } else {
370            return Err(NmstateError::new(
371                ErrorKind::InvalidArgument,
372                format!(
373                    "Invalid DNS server {srv}, the IPv6 link local DNS server \
374                     should be in the format like 'fe80::deef:1%eth1'"
375                ),
376            ));
377        }
378    }
379    Ok(None)
380}
381
382impl MergedInterface {
383    // IP stack is merged with current at this point.
384    pub(crate) fn is_iface_valid_for_dns(&self, is_ipv6: bool) -> bool {
385        if !self.merged.is_up() {
386            return false;
387        }
388        if is_ipv6 {
389            self.merged.base_iface().ipv6.as_ref().map(|ip_conf| {
390                ip_conf.enabled && (ip_conf.is_static() || (ip_conf.is_auto()))
391            }) == Some(true)
392        } else {
393            self.merged.base_iface().ipv4.as_ref().map(|ip_conf| {
394                ip_conf.enabled && (ip_conf.is_static() || (ip_conf.is_auto()))
395            }) == Some(true)
396        }
397    }
398}