1use 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)]
37pub struct DnsState {
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub running: Option<DnsClientState>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub config: Option<DnsClientState>,
78}
79
80impl DnsState {
81 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)]
101pub struct DnsClientState {
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub server: Option<Vec<String>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub search: Option<Vec<String>>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub options: Option<Vec<String>>,
118 #[serde(skip)]
119 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 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 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 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 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}