Skip to content

Commit

Permalink
dns: Support IPv6 link local address as DNS nameserver
Browse files Browse the repository at this point in the history
When using IPv6 link local address as DNS nameserver, interface name is
required, for example: `fe80::deef:1%eth1`

Example yaml:

```yml
---
dns-resolver:
  config:
    search:
    - example.com
    - example.org
    server:
    - fe80::deef:1%eth1
    - 2001:4860:4860::8844
    - 8.8.4.4
    - 8.8.8.8
```

Restrictions:
 * You cannot have 2+ interface names in DNS server names. Massive work
   required to handle the DNS ordering between interfaces with the
   design of NetworkManager.

 * All other normal IPv6 nameserver will also stored in link local
   interface if exists.

Unit test cases and integration test case included.

Signed-off-by: Gris Ge <fge@redhat.com>
  • Loading branch information
cathay4t authored and ffmancera committed Nov 17, 2022
1 parent c5e12bc commit 0915c91
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 41 deletions.
185 changes: 155 additions & 30 deletions rust/src/lib/dns.rs
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: Apache-2.0

use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::{
Expand Down Expand Up @@ -228,42 +230,50 @@ pub(crate) fn reselect_dns_ifaces(
desired: &NetworkState,
current: &NetworkState,
) -> (String, String) {
(
find_ifaces_in_desire(false, &desired.interfaces)
.or_else(|| {
find_valid_ifaces_for_dns(
false,
&desired.interfaces,
&current.interfaces,
)
})
.unwrap_or_default(),
find_ifaces_in_desire(true, &desired.interfaces)
.or_else(|| {
find_valid_ifaces_for_dns(
true,
&desired.interfaces,
&current.interfaces,
)
})
.unwrap_or_default(),
let ipv4_iface = find_ifaces_in_desire(false, &desired.interfaces)
.or_else(|| {
find_valid_ifaces_for_dns(
false,
&desired.interfaces,
&current.interfaces,
)
})
.unwrap_or_default();

let ipv6_iface = extra_ipv6_link_local_iface_from_dns_srv(
desired
.dns
.config
.as_ref()
.and_then(|c| c.server.as_deref()),
)
.or_else(|| find_ifaces_in_desire(true, &desired.interfaces))
.or_else(|| {
find_valid_ifaces_for_dns(
true,
&desired.interfaces,
&current.interfaces,
)
})
.unwrap_or_default();

(ipv4_iface, ipv6_iface)
}

// Return None if specified interface has IP configuration as None.
fn is_iface_valid_for_dns(is_ipv6: bool, iface: &Interface) -> Option<bool> {
// IP stack is merged with current at this point.
fn is_iface_valid_for_dns(is_ipv6: bool, iface: &Interface) -> bool {
if is_ipv6 {
iface.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 {
iface.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)
}
}

Expand All @@ -278,7 +288,7 @@ fn current_dns_ifaces_are_still_valid(
if let Some(des_iface) =
desired.interfaces.kernel_ifaces.get(iface_name)
{
if is_iface_valid_for_dns(false, des_iface) == Some(false) {
if !is_iface_valid_for_dns(false, des_iface) {
return false;
}
}
Expand All @@ -289,7 +299,7 @@ fn current_dns_ifaces_are_still_valid(
if let Some(des_iface) =
desired.interfaces.kernel_ifaces.get(iface_name)
{
if is_iface_valid_for_dns(true, des_iface) == Some(false) {
if !is_iface_valid_for_dns(true, des_iface) {
return false;
}
}
Expand All @@ -305,7 +315,7 @@ fn find_ifaces_in_desire(
desired: &Interfaces,
) -> Option<String> {
for (iface_name, iface) in desired.kernel_ifaces.iter() {
if is_iface_valid_for_dns(is_ipv6, iface) == Some(true) {
if is_iface_valid_for_dns(is_ipv6, iface) {
return Some(iface_name.to_string());
}
}
Expand All @@ -322,10 +332,10 @@ fn find_valid_ifaces_for_dns(
.iter()
.chain(current.kernel_ifaces.iter())
{
if is_iface_valid_for_dns(is_ipv6, iface) == Some(true) {
if is_iface_valid_for_dns(is_ipv6, iface) {
let des_iface = desired.kernel_ifaces.get(iface_name);
if let Some(des_iface) = des_iface {
if is_iface_valid_for_dns(is_ipv6, des_iface) != Some(false) {
if is_iface_valid_for_dns(is_ipv6, des_iface) {
return Some(iface_name.to_string());
}
} else {
Expand Down Expand Up @@ -445,7 +455,9 @@ pub(crate) fn purge_dns_config(
}
}

// Only preferred: true will save the searches
// Argument `preferred`: true will save the searches
// Assuming all IPv6 link local address is pointing to specified argument
// `iface_name` iface.
fn _save_dns_to_iface(
is_ipv6: bool,
iface_name: &str,
Expand All @@ -455,7 +467,13 @@ fn _save_dns_to_iface(
current: &NetworkState,
preferred: bool,
) -> Result<(), NmstateError> {
let (servers, searches) = dns_conf;
let (mut servers, searches) = dns_conf;
for srv in servers.as_mut_slice() {
if let Some((ip, _)) = parse_dns_ipv6_link_local_srv(srv)? {
srv.replace_range(.., ip.to_string().as_str());
}
}

if iface_name.is_empty() {
let e = NmstateError::new(
ErrorKind::InvalidArgument,
Expand Down Expand Up @@ -538,3 +556,110 @@ fn _save_dns_to_iface(

Ok(())
}

// * Specified interface is valid for hold IPv6 DNS config.
// * Cannot have more than one IPv6 link-local DNS interface.
pub(crate) fn validate_ipv6_link_local_address_dns_srv(
desired: &NetworkState,
current: &NetworkState,
) -> Result<(), NmstateError> {
let mut iface_names = Vec::new();
if let Some(srvs) =
desired.dns.config.as_ref().and_then(|c| c.server.as_ref())
{
for srv in srvs {
if let Some((_, iface_name)) = parse_dns_ipv6_link_local_srv(srv)? {
let iface = if let Some(iface) =
desired.interfaces.kernel_ifaces.get(iface_name).or_else(
|| current.interfaces.kernel_ifaces.get(iface_name),
) {
iface
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Desired IPv6 link local DNS server {} is \
pointing to interface {} which does not exist.",
srv, iface_name
),
));
};
if is_iface_valid_for_dns(true, iface) {
iface_names.push(iface.name());
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Interface {} has IPv6 disabled, \
hence cannot hold desired IPv6 link local \
DNS server {}",
iface_name, 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(())
}

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 {}, only IPv6 link local \
address is allowed to have '%' character in DNS \
name server, the correct format should be \
'fe80::deef:1%eth1'",
srv
),
));
}
}
} else {
return Err(NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Invalid DNS server {}, the IPv6 \
link local DNS server should be in the format like \
'fe80::deef:1%eth1'",
srv
),
));
}
}
Ok(None)
}

fn extra_ipv6_link_local_iface_from_dns_srv(
srvs: Option<&[String]>,
) -> Option<String> {
if let Some(srvs) = srvs {
for srv in srvs {
let splits: Vec<&str> = srv.split('%').collect();
if splits.len() == 2 && !splits[1].is_empty() {
return Some(splits[1].to_string());
}
}
}
None
}
4 changes: 3 additions & 1 deletion rust/src/lib/net_state.rs
Expand Up @@ -8,7 +8,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use crate::{
dns::{
get_cur_dns_ifaces, is_dns_changed, purge_dns_config,
reselect_dns_ifaces,
reselect_dns_ifaces, validate_ipv6_link_local_address_dns_srv,
},
DnsState, ErrorKind, HostNameState, Interface, InterfaceType, Interfaces,
NmstateError, OvsDbGlobalConfig, RouteRules, Routes,
Expand Down Expand Up @@ -531,6 +531,8 @@ impl NetworkState {
let mut self_clone = self.clone();
self_clone.dns.merge_current(&current.dns);

validate_ipv6_link_local_address_dns_srv(&self_clone, current)?;

if is_dns_changed(&self_clone, current) {
let (v4_iface_name, v6_iface_name) =
reselect_dns_ifaces(&self_clone, current);
Expand Down
56 changes: 51 additions & 5 deletions rust/src/lib/nm/query/dns.rs
@@ -1,15 +1,42 @@
// SPDX-License-Identifier: Apache-2.0

use std::str::FromStr;

use super::super::{
error::nm_error_to_nmstate,
nm_dbus::{NmApi, NmSettingIp},
nm_dbus::{NmApi, NmDnsEntry, NmSettingIp},
};

use crate::{
ip::is_ipv6_unicast_link_local, DnsClientState, DnsState, Interfaces,
NmstateError,
};

use crate::{DnsClientState, DnsState, Interfaces, NmstateError};
pub(crate) fn nm_dns_to_nmstate(
iface_name: &str,
nm_ip_setting: &NmSettingIp,
) -> DnsClientState {
let mut servers = Vec::new();
if let Some(srvs) = nm_ip_setting.dns.as_ref() {
for srv in srvs {
if let Ok(ip) = std::net::Ipv6Addr::from_str(srv.as_str()) {
if is_ipv6_unicast_link_local(&ip) {
servers.push(format!("{}%{}", srv, iface_name));
} else {
servers.push(srv.to_string());
}
} else {
servers.push(srv.to_string());
}
}
}

pub(crate) fn nm_dns_to_nmstate(nm_ip_setting: &NmSettingIp) -> DnsClientState {
DnsClientState {
server: nm_ip_setting.dns.clone(),
server: if nm_ip_setting.dns.is_none() {
None
} else {
Some(servers)
},
search: nm_ip_setting.dns_search.clone(),
priority: nm_ip_setting.dns_priority,
}
Expand All @@ -26,7 +53,7 @@ pub(crate) fn retrieve_dns_info(
let mut running_srvs: Vec<String> = Vec::new();
let mut running_schs: Vec<String> = Vec::new();
for nm_dns_entry in nm_dns_entires {
running_srvs.extend_from_slice(nm_dns_entry.name_servers.as_slice());
running_srvs.extend(nm_dns_srvs_to_nmstate(&nm_dns_entry));
running_schs.extend_from_slice(nm_dns_entry.domains.as_slice());
}

Expand Down Expand Up @@ -76,3 +103,22 @@ pub(crate) fn retrieve_dns_info(
}),
})
}

fn nm_dns_srvs_to_nmstate(nm_dns_entry: &NmDnsEntry) -> Vec<String> {
let mut srvs = Vec::new();
for srv in nm_dns_entry.name_servers.as_slice() {
if let Ok(ip) = std::net::Ipv6Addr::from_str(srv.as_str()) {
if is_ipv6_unicast_link_local(&ip)
&& !nm_dns_entry.interface.is_empty()
{
srvs.push(format!("{}%{}", srv, nm_dns_entry.interface));
continue;
} else {
srvs.push(srv.to_string());
}
} else {
srvs.push(srv.to_string());
}
}
srvs
}
5 changes: 3 additions & 2 deletions rust/src/lib/nm/query/ip.rs
Expand Up @@ -51,7 +51,7 @@ pub(crate) fn nm_ip_setting_to_nmstate4(
"auto_table_id",
"auto_route_metric",
],
dns: Some(nm_dns_to_nmstate(nm_ip_setting)),
dns: Some(nm_dns_to_nmstate("", nm_ip_setting)),
dhcp_client_id: nm_dhcp_client_id_to_nmstate(nm_ip_setting),
auto_route_metric: nm_ip_setting.route_metric.map(|i| i as u32),
..Default::default()
Expand All @@ -62,6 +62,7 @@ pub(crate) fn nm_ip_setting_to_nmstate4(
}

pub(crate) fn nm_ip_setting_to_nmstate6(
iface_name: &str,
nm_ip_setting: &NmSettingIp,
) -> InterfaceIpv6 {
if let Some(nm_ip_method) = &nm_ip_setting.method {
Expand Down Expand Up @@ -97,7 +98,7 @@ pub(crate) fn nm_ip_setting_to_nmstate6(
"addr_gen_mode",
"auto_route_metric",
],
dns: Some(nm_dns_to_nmstate(nm_ip_setting)),
dns: Some(nm_dns_to_nmstate(iface_name, nm_ip_setting)),
dhcp_duid: nm_dhcp_duid_to_nmstate(nm_ip_setting),
addr_gen_mode: {
if enabled {
Expand Down
5 changes: 4 additions & 1 deletion rust/src/lib/nm/show.rs
Expand Up @@ -218,7 +218,10 @@ fn nm_conn_to_base_iface(
) -> Option<BaseInterface> {
if let Some(iface_name) = nm_conn.iface_name() {
let ipv4 = nm_conn.ipv4.as_ref().map(nm_ip_setting_to_nmstate4);
let ipv6 = nm_conn.ipv6.as_ref().map(nm_ip_setting_to_nmstate6);
let ipv6 = nm_conn
.ipv6
.as_ref()
.map(|nm_ip_set| nm_ip_setting_to_nmstate6(iface_name, nm_ip_set));

let mut base_iface = BaseInterface::new();
base_iface.name = iface_name.to_string();
Expand Down

0 comments on commit 0915c91

Please sign in to comment.