use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::{
ip::is_ipv6_addr, ErrorKind, MergedInterface, MergedNetworkState,
NmstateError,
};
#[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()
}
}
#[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)]
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()
}
pub(crate) fn is_purge(&self) -> bool {
match (&self.server, &self.search) {
(Some(srvs), Some(schs)) => srvs.is_empty() && schs.is_empty(),
(Some(srvs), None) => srvs.is_empty(),
(None, Some(schs)) => schs.is_empty(),
(None, None) => true,
}
}
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
}
}
fn is_mixed_dns_servers(srvs: &[String]) -> bool {
let mut pattern = String::new();
for srv in srvs {
let cur_char = if is_ipv6_addr(srv) { '6' } else { '4' };
if !pattern.ends_with(cur_char) {
pattern.push(cur_char);
}
}
pattern.contains("464") || pattern.contains("646")
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct MergedDnsState {
desired: DnsState,
current: DnsState,
pub(crate) servers: Vec<String>,
pub(crate) searches: Vec<String>,
}
impl MergedDnsState {
pub(crate) fn new(
desired: DnsState,
current: DnsState,
) -> Result<Self, NmstateError> {
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();
if let Some(conf) = desired.config.as_ref() {
if conf.is_purge() {
servers.clear();
searches.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);
}
}
}
let mut sanitized_srvs = Vec::new();
for srv in servers {
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}",),
));
}
}
validate_dns_srvs(sanitized_srvs.as_slice())?;
Ok(Self {
desired,
current,
servers: sanitized_srvs,
searches,
})
}
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();
self.servers != cur_servers || self.searches != cur_searches
}
}
fn validate_dns_srvs(srvs: &[String]) -> Result<(), NmstateError> {
if srvs.len() > 2 && is_mixed_dns_servers(srvs) {
let e = NmstateError::new(
ErrorKind::NotImplementedError,
"Placing IPv4/IPv6 nameserver in the middle of IPv6/IPv4 \
nameservers is not supported yet"
.to_string(),
);
log::error!("{}", e);
return Err(e);
}
Ok(())
}
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()
&& ip_conf.auto_dns == Some(false)))
}) == Some(true)
} else {
self.merged.base_iface().ipv4.as_ref().map(|ip_conf| {
ip_conf.enabled
&& (ip_conf.is_static()
|| (ip_conf.is_auto()
&& ip_conf.auto_dns == Some(false)))
}) == Some(true)
}
}
}