From 1577e1073593d19cdaa7de2eadfc893978cab255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 18 Jul 2025 14:22:41 +0200 Subject: [PATCH 1/5] Fix interface state detection when NM device is "unavailable" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A device that is not ready to connect may have the NetworkManager's state "unavailable" in some circumstances. Consider this as `state: down`, as we do for the "disconnected" state. Fixes https://github.com/nmstate/nmstate/issues/2798 Signed-off-by: Íñigo Huguet --- rust/src/lib/nm/show.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/src/lib/nm/show.rs b/rust/src/lib/nm/show.rs index 78ff66a1..d4dd575f 100644 --- a/rust/src/lib/nm/show.rs +++ b/rust/src/lib/nm/show.rs @@ -285,7 +285,9 @@ fn nm_dev_to_nm_iface(nm_dev: &NmDevice) -> Option { base_iface.state = InterfaceState::Ignore; } } - NmDeviceState::Disconnected => base_iface.state = InterfaceState::Down, + NmDeviceState::Disconnected | NmDeviceState::Unavailable => { + base_iface.state = InterfaceState::Down + } _ => base_iface.state = InterfaceState::Up, } base_iface.iface_type = nm_dev_iface_type_to_nmstate(nm_dev); -- 2.49.0 From f09fc1a5efc589b5740922cdfcc68ba29b94406e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 18 Jul 2025 16:12:18 +0200 Subject: [PATCH 2/5] Fix interface state detection in kernel mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any device with administrative state UP was being considered as `state: up` in kernel mode. This is not correct because if the operational state is not Up, for example because of the cable being disconnected, nobody would think that the device is **really** up. Fix it by ignoring the administrative state and consider only the operational state. Fixes https://github.com/nmstate/nmstate/issues/2798 Signed-off-by: Íñigo Huguet --- rust/src/lib/nispor/base_iface.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/src/lib/nispor/base_iface.rs b/rust/src/lib/nispor/base_iface.rs index f7f3e507..4b04abdb 100644 --- a/rust/src/lib/nispor/base_iface.rs +++ b/rust/src/lib/nispor/base_iface.rs @@ -37,8 +37,11 @@ fn np_iface_type_to_nmstate( impl From<(&nispor::IfaceState, &[nispor::IfaceFlag])> for InterfaceState { fn from(tuple: (&nispor::IfaceState, &[nispor::IfaceFlag])) -> Self { let (state, flags) = tuple; + // nispor::IfaceState::Up means operational up. + // Check also the Running flag with, according to [1], means operational + // state Up or Unknown. + // [1] https://www.kernel.org/doc/Documentation/networking/operstates.txt if *state == nispor::IfaceState::Up - || flags.contains(&nispor::IfaceFlag::Up) || flags.contains(&nispor::IfaceFlag::Running) { InterfaceState::Up -- 2.49.0 From 6f7692f9900499d75548eda250487d09f59c357c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 18 Jul 2025 09:38:32 +0200 Subject: [PATCH 3/5] dns: don't add DNS search and/or options to down NM connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When configuring only search and/or options in dns-resolver we add them to a NM connection and not to NM's global config, but nmstate may choose a down interface, which results on errors at apply time or even on the configurations being accepted but stay unused by NetworkManager. Fix that by always checking that the interface is valid for DNS, and by adding the requirement of the interface being up. Signed-off-by: Íñigo Huguet --- rust/src/lib/dns.rs | 3 ++ rust/src/lib/nm/dns.rs | 120 ++++++++++++++++++++++------------------- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/rust/src/lib/dns.rs b/rust/src/lib/dns.rs index 1aae91ff..e3025ed3 100644 --- a/rust/src/lib/dns.rs +++ b/rust/src/lib/dns.rs @@ -382,6 +382,9 @@ pub(crate) fn parse_dns_ipv6_link_local_srv( impl MergedInterface { // IP stack is merged with current at this point. pub(crate) fn is_iface_valid_for_dns(&self, is_ipv6: bool) -> bool { + if !self.merged.is_up() { + return false; + } if is_ipv6 { self.merged.base_iface().ipv6.as_ref().map(|ip_conf| { ip_conf.enabled && (ip_conf.is_static() || (ip_conf.is_auto())) diff --git a/rust/src/lib/nm/dns.rs b/rust/src/lib/nm/dns.rs index a180178a..3c45f78b 100644 --- a/rust/src/lib/nm/dns.rs +++ b/rust/src/lib/nm/dns.rs @@ -693,13 +693,14 @@ fn store_dns_search_or_options_to_auto_iface( Some(i) => i, None => continue, }; - if iface - .merged - .base_iface() - .ipv6 - .as_ref() - .map(|i| i.is_auto()) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(true) + && iface + .merged + .base_iface() + .ipv6 + .as_ref() + .map(|i| i.is_auto()) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -708,13 +709,14 @@ fn store_dns_search_or_options_to_auto_iface( true, ); } - if iface - .merged - .base_iface() - .ipv4 - .as_ref() - .map(|i| i.is_auto()) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(false) + && iface + .merged + .base_iface() + .ipv4 + .as_ref() + .map(|i| i.is_auto()) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -754,13 +756,14 @@ fn store_dns_search_or_options_to_auto_iface( Some(i) => i, None => continue, }; - if iface - .merged - .base_iface() - .ipv6 - .as_ref() - .map(|i| i.is_auto()) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(true) + && iface + .merged + .base_iface() + .ipv6 + .as_ref() + .map(|i| i.is_auto()) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -769,13 +772,14 @@ fn store_dns_search_or_options_to_auto_iface( true, ); } - if iface - .merged - .base_iface() - .ipv4 - .as_ref() - .map(|i| i.is_auto()) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(false) + && iface + .merged + .base_iface() + .ipv4 + .as_ref() + .map(|i| i.is_auto()) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -821,13 +825,14 @@ fn store_dns_search_or_options_to_ip_enabled_iface( Some(i) => i, None => continue, }; - if iface - .merged - .base_iface() - .ipv6 - .as_ref() - .map(|i| i.enabled) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(true) + && iface + .merged + .base_iface() + .ipv6 + .as_ref() + .map(|i| i.enabled) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -836,13 +841,14 @@ fn store_dns_search_or_options_to_ip_enabled_iface( true, ); } - if iface - .merged - .base_iface() - .ipv4 - .as_ref() - .map(|i| i.enabled) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(false) + && iface + .merged + .base_iface() + .ipv4 + .as_ref() + .map(|i| i.enabled) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -882,13 +888,14 @@ fn store_dns_search_or_options_to_ip_enabled_iface( Some(i) => i, None => continue, }; - if iface - .merged - .base_iface() - .ipv6 - .as_ref() - .map(|i| i.enabled) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(true) + && iface + .merged + .base_iface() + .ipv6 + .as_ref() + .map(|i| i.enabled) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, @@ -897,13 +904,14 @@ fn store_dns_search_or_options_to_ip_enabled_iface( true, ); } - if iface - .merged - .base_iface() - .ipv4 - .as_ref() - .map(|i| i.enabled) - .unwrap_or_default() + if iface.is_iface_valid_for_dns(false) + && iface + .merged + .base_iface() + .ipv4 + .as_ref() + .map(|i| i.enabled) + .unwrap_or_default() { return set_iface_dns_search_or_option( iface, -- 2.49.0 From ea08a8dc2b5f6d39f9199e6484a289e2621b0b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Wed, 23 Jul 2025 16:18:27 +0200 Subject: [PATCH 4/5] nm dns: purge ifaces before saving new config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In store_dns_config_to_iface we correctly purge the current DNS configurations from NM profiles before saving the new config, ensuring that no old leftover configs remain afterwards. Do the same in store_dns_search_or_option_to_iface so the DNS config is correctly applied when only DNS searches or options are defined too. Without this fix, the configuration fails in the following scenario: - A deactivated NM profile/device contains a dns-search - A desired state with only DNS searches or options is applied - The new state is saved to an activated interface, but the dns-search from the deactivated one is not removed as expected. $ nmcli c show downiface | grep dns-search ipv4.dns-search: example.com ipv6.dns-search: -- $ nmcli c show upiface | grep dns-search ipv4.dns-search: -- ipv6.dns-search: -- $ cat desired.yml dns-resolver: config: search: ["example2.com"] server: [] $ nmstatectl apply desired.yml ... Retrying on: VerificationError: Failed to apply DNS config: desire searches 'example2.com', got 'example.com example2.com' ... $ nmstate apply --no-verify desired.yml ... $ nmcli c show downiface | grep dns-search ipv4.dns-search: example.com <-- NOT PURGED! ipv6.dns-search: -- $ nmcli c show upiface | grep dns-search ipv4.dns-search: -- ipv6.dns-search: example2.com Signed-off-by: Íñigo Huguet --- rust/src/lib/nm/dns.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/src/lib/nm/dns.rs b/rust/src/lib/nm/dns.rs index 3c45f78b..7e125579 100644 --- a/rust/src/lib/nm/dns.rs +++ b/rust/src/lib/nm/dns.rs @@ -579,6 +579,10 @@ pub(crate) fn store_dns_search_or_option_to_iface( let (cur_v4_ifaces, cur_v6_ifaces) = get_cur_dns_ifaces(&merged_state.interfaces); + // First purge existing configs + purge_dns_config(false, &cur_v4_ifaces, merged_state)?; + purge_dns_config(true, &cur_v6_ifaces, merged_state)?; + // Use current DNS interface if they are desired for iface_name in cur_v6_ifaces { if let Some(iface) = -- 2.49.0 From 66c6aff0d4fe4873515a898f3f58b752b054fdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 24 Jul 2025 10:26:49 +0200 Subject: [PATCH 5/5] test: nm dns: add test for DNS searches in down iface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Íñigo Huguet --- tests/integration/nm/dns_test.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/integration/nm/dns_test.py b/tests/integration/nm/dns_test.py index 57a5e1d5..2062b27f 100644 --- a/tests/integration/nm/dns_test.py +++ b/tests/integration/nm/dns_test.py @@ -328,3 +328,36 @@ def test_write_both_global_dns_and_iface_dns(eth1_up): assert cmdlib.exec_cmd( "nmcli -g ipv4.dns c show eth1".split(), check=True )[1].strip() == ",".join(TEST_DNS_SRVS) + + +# https://issues.redhat.com/browse/RHEL-102333 +def test_set_dns_search_only_in_down_iface(auto_eth1, eth2_up): + # Add a DNS search domain to a down interface + cmdlib.exec_cmd("nmcli device down eth2".split(), check=True) + cmdlib.exec_cmd("nmcli c modify eth2 ipv4.method auto".split(), check=True) + cmdlib.exec_cmd( + "nmcli c modify eth2 ipv4.dns-search 'example.com'".split(), + check=True, + ) + + # Assert that the DNS searches configuration can change correctly + libnmstate.apply( + { + DNS.KEY: {DNS.CONFIG: {DNS.SEARCH: ["example2.com"]}}, + } + ) + + # Assert that the old configuration has been purged from the down interface + # and the new one hasn't been added to it. + _r, search4, _e = cmdlib.exec_cmd( + "nmcli -g ipv4.dns-search c show eth2".split(), check=True + ) + _r, search6, _e = cmdlib.exec_cmd( + "nmcli -g ipv6.dns-search c show eth2".split(), check=True + ) + assert "example.com" not in search4 and "example.com" not in search6 + assert "example2.com" not in search4 and "example2.com" not in search6 + + # It is not possible to assert that the new configuration has been put into + # eth1 because, if there are more interfaces in the host, it might be in + # any of them -- 2.49.0