use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::{
ip::is_ipv6_addr, ErrorKind, MergedInterface, MergedNetworkState,
NmstateError,
};
const SUPPORTED_DNS_OPTS_NO_VALUE: [&str; 15] = [
"debug",
"edns0",
"inet6",
"ip6-bytestring",
"ip6-dotint",
"no-aaaa",
"no-check-names",
"no-ip6-dotint",
"no-reload",
"no-tld-query",
"rotate",
"single-request",
"single-request-reopen",
"trust-ad",
"use-vc",
];
const SUPPORTED_DNS_OPTS_WITH_VALUE: [&str; 3] =
["ndots", "timeout", "attempts"];
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct DnsState {
#[serde(skip_serializing_if = "Option::is_none")]
pub running: Option<DnsClientState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<DnsClientState>,
}
impl DnsState {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.running.is_none() && self.config.is_none()
}
pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
if let Some(config) = self.config.as_mut() {
config.sanitize()?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct DnsClientState {
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<String>>,
#[serde(skip)]
pub(crate) priority: Option<i32>,
}
impl DnsClientState {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.server.is_none() && self.search.is_none() && self.options.is_none()
}
pub(crate) fn is_purge(&self) -> bool {
self.server.is_none() && self.search.is_none() & self.options.is_none()
|| self.server.as_deref() == Some(&[])
&& self.search.as_deref() == Some(&[])
&& self.options.as_deref() == Some(&[])
}
pub(crate) fn is_null(&self) -> bool {
self.server.as_ref().map(|s| s.len()).unwrap_or_default() == 0
&& self.search.as_ref().map(|s| s.len()).unwrap_or_default() == 0
&& self.options.as_ref().map(|s| s.len()).unwrap_or_default() == 0
}
pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
if let Some(srvs) = self.server.as_mut() {
let mut sanitized_srvs = Vec::new();
for srv in srvs {
if is_ipv6_addr(srv.as_str()) {
let splits: Vec<&str> = srv.split('%').collect();
if splits.len() == 2 {
if let Ok(ip_addr) = splits[0].parse::<Ipv6Addr>() {
sanitized_srvs
.push(format!("{}%{}", ip_addr, splits[1]));
}
} else if let Ok(ip_addr) = srv.parse::<Ipv6Addr>() {
sanitized_srvs.push(ip_addr.to_string());
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!("Invalid DNS server string {srv}",),
));
}
} else if let Ok(ip_addr) = srv.parse::<Ipv4Addr>() {
sanitized_srvs.push(ip_addr.to_string());
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!("Invalid DNS server string {srv}",),
));
}
}
self.server = Some(sanitized_srvs);
}
if let Some(opts) = self.options.as_ref() {
for opt in opts {
match opt.find(':') {
Some(i) => {
let opt = &opt[..i];
if !SUPPORTED_DNS_OPTS_WITH_VALUE.contains(&opt) {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Option '{opt}' is not supported to hold \
a value, only support these without \
value: {} and these with values: {}:n",
SUPPORTED_DNS_OPTS_NO_VALUE.join(", "),
SUPPORTED_DNS_OPTS_WITH_VALUE.join(":n, ")
),
));
}
}
None => {
if !SUPPORTED_DNS_OPTS_NO_VALUE.contains(&opt.as_str())
{
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Unsupported DNS option {opt}, \
only support these without value: {} \
and these with values: {}",
SUPPORTED_DNS_OPTS_NO_VALUE.join(", "),
SUPPORTED_DNS_OPTS_WITH_VALUE.join(":n, ")
),
));
}
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct MergedDnsState {
pub(crate) desired: DnsState,
pub(crate) current: DnsState,
pub(crate) servers: Vec<String>,
pub(crate) searches: Vec<String>,
pub(crate) options: Vec<String>,
}
impl MergedDnsState {
pub(crate) fn new(
mut desired: DnsState,
mut current: DnsState,
) -> Result<Self, NmstateError> {
desired.sanitize()?;
current.sanitize().ok();
let mut servers = current
.config
.as_ref()
.and_then(|c| c.server.clone())
.unwrap_or_default();
let mut searches = current
.config
.as_ref()
.and_then(|c| c.search.clone())
.unwrap_or_default();
let mut options = current
.config
.as_ref()
.and_then(|c| c.options.clone())
.unwrap_or_default();
if let Some(conf) = desired.config.as_ref() {
if conf.is_purge() {
servers.clear();
searches.clear();
options.clear();
} else {
if let Some(des_srvs) = conf.server.as_ref() {
servers.clear();
servers.extend_from_slice(des_srvs);
}
if let Some(des_schs) = conf.search.as_ref() {
searches.clear();
searches.extend_from_slice(des_schs);
}
if let Some(des_opts) = conf.options.as_ref() {
options.clear();
options.extend_from_slice(des_opts);
}
}
}
Ok(Self {
desired,
current,
servers,
searches,
options,
})
}
pub(crate) fn is_changed(&self) -> bool {
let cur_servers = self
.current
.config
.as_ref()
.and_then(|c| c.server.clone())
.unwrap_or_default();
let cur_searches = self
.current
.config
.as_ref()
.and_then(|c| c.search.clone())
.unwrap_or_default();
let cur_options = self
.current
.config
.as_ref()
.and_then(|c| c.options.clone())
.unwrap_or_default();
self.servers != cur_servers
|| self.searches != cur_searches
|| self.options != cur_options
}
pub(crate) fn is_search_or_option_only(&self) -> bool {
self.servers.is_empty()
&& (!self.searches.is_empty() || !self.options.is_empty())
}
}
impl MergedNetworkState {
pub(crate) fn validate_ipv6_link_local_address_dns_srv(
&self,
) -> Result<(), NmstateError> {
let mut iface_names = Vec::new();
for srv in self.dns.servers.as_slice() {
if let Some((_, iface_name)) = parse_dns_ipv6_link_local_srv(srv)? {
let iface = if let Some(iface) =
self.interfaces.kernel_ifaces.get(iface_name)
{
iface
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Desired IPv6 link local DNS server {srv} is \
pointing to interface {iface_name} \
which does not exist."
),
));
};
if iface.is_iface_valid_for_dns(true) {
iface_names.push(iface.merged.name());
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Interface {iface_name} has IPv6 disabled, \
hence cannot hold desired IPv6 link local \
DNS server {srv}"
),
));
}
}
}
if iface_names.len() >= 2 {
return Err(NmstateError::new(
ErrorKind::NotImplementedError,
format!(
"Only support IPv6 link local DNS name server(s) \
pointing to a single interface, but got '{}'",
iface_names.join(" ")
),
));
}
Ok(())
}
}
pub(crate) fn parse_dns_ipv6_link_local_srv(
srv: &str,
) -> Result<Option<(std::net::Ipv6Addr, &str)>, NmstateError> {
if srv.contains('%') {
let splits: Vec<&str> = srv.split('%').collect();
if splits.len() == 2 {
match std::net::Ipv6Addr::from_str(splits[0]) {
Ok(ip) => return Ok(Some((ip, splits[1]))),
Err(_) => {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Invalid IPv6 address in {srv}, only IPv6 link local \
address is allowed to have '%' character in DNS \
name server, the correct format should be \
'fe80::deef:1%eth1'"
),
));
}
}
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Invalid DNS server {srv}, the IPv6 \
link local DNS server should be in the format like \
'fe80::deef:1%eth1'"
),
));
}
}
Ok(None)
}
impl MergedInterface {
pub(crate) fn is_iface_valid_for_dns(&self, is_ipv6: bool) -> bool {
if is_ipv6 {
self.merged.base_iface().ipv6.as_ref().map(|ip_conf| {
ip_conf.enabled && (ip_conf.is_static() || (ip_conf.is_auto()))
}) == Some(true)
} else {
self.merged.base_iface().ipv4.as_ref().map(|ip_conf| {
ip_conf.enabled && (ip_conf.is_static() || (ip_conf.is_auto()))
}) == Some(true)
}
}
}