diff --git a/nmrs/CHANGELOG.md b/nmrs/CHANGELOG.md index cfa21cf0..10221edf 100644 --- a/nmrs/CHANGELOG.md +++ b/nmrs/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to the `nmrs` crate will be documented in this file. - Credential-bearing Wi-Fi, EAP, OpenVPN, and WireGuard types now redact passwords and private keys from debug output, and connection tracing no longer logs raw settings, without changing public field types. ([#508](https://github.com/freedesktop-rs/nmrs/pull/508)) +- Non-palindromic IPv4 DNS server addresses are no longer rejected by the + OpenVPN and WireGuard builders. ([#509](https://github.com/freedesktop-rs/nmrs/pull/519)) ### Fixed @@ -16,6 +18,7 @@ All notable changes to the `nmrs` crate will be documented in this file. instead of assuming the adapter is `hci0`. ([#501](https://github.com/freedesktop-rs/nmrs/pull/501)) ## [3.4.1] - 2026-07-19 + ### Added - Isolated NetworkManager integration contracts now cover saved-profile events, @@ -57,7 +60,9 @@ All notable changes to the `nmrs` crate will be documented in this file. decoding, error reporting, and temporary-file cleanup. ([#505](https://github.com/freedesktop-rs/nmrs/pull/505)) ## [3.4.0] - 2026-07-08 -### Added + +### Added + - Expose existing secrets on SecretRequest for re-auth prefill ([#460](https://github.com/freedesktop-rs/nmrs/pull/460)) - `MonitorHandle` returned by `monitor_network_changes` and `monitor_device_changes` for graceful shutdown ([#461](https://github.com/freedesktop-rs/nmrs/pull/461)) - `NetworkManager::dbus_connection()` and `nmrs::raw` (`zbus` / `zvariant` re-exports) for advanced builder workflows ([#462](https://github.com/freedesktop-rs/nmrs/pull/464)) @@ -65,13 +70,16 @@ All notable changes to the `nmrs` crate will be documented in this file. - mdbook docs for builder submission workflow, `add_connection()`, and `add_and_activate_connection()` ([#462](https://github.com/freedesktop-rs/nmrs/pull/464)) ### Fixed + - `monitor_network_changes` now detects hotplugged Wi-Fi devices instead of only monitoring devices present at startup ([#461](https://github.com/freedesktop-rs/nmrs/pull/461)) - Monitors return `Ok(())` on clean shutdown instead of always returning `Err(Stuck(...))` ([#461](https://github.com/freedesktop-rs/nmrs/pull/461)) ### Changed + - **Breaking:** `monitor_network_changes` and `monitor_device_changes` now return `Result` instead of `Result<()>` ([#461](https://github.com/freedesktop-rs/nmrs/pull/461)) ## [3.3.0] - 2026-06-30 + ### Added - `Device::speed_mbps` and `NetworkManager::list_wired_device_details()` expose diff --git a/nmrs/src/api/builders/connection_builder.rs b/nmrs/src/api/builders/connection_builder.rs index 6b635e88..f6650e94 100644 --- a/nmrs/src/api/builders/connection_builder.rs +++ b/nmrs/src/api/builders/connection_builder.rs @@ -297,10 +297,18 @@ impl ConnectionBuilder { /// Sets IPv4 DNS servers. /// - /// DNS servers are specified as integers (network byte order). + /// NetworkManager's legacy `ipv4.dns` property is an array of `in_addr_t`, + /// i.e. each `u32`'s in-memory bytes are the address octets in order + /// (network byte order). `u32::from(Ipv4Addr)` yields the *host*-order + /// integer instead, which NetworkManager reads back with the octets + /// reversed on little-endian machines, so build the integer from the raw + /// octets in native byte order. #[must_use] pub fn ipv4_dns(mut self, servers: Vec) -> Self { - let dns_u32: Vec = servers.into_iter().map(u32::from).collect(); + let dns_u32: Vec = servers + .into_iter() + .map(|server| u32::from_ne_bytes(server.octets())) + .collect(); if let Some(ipv4) = self.settings.get_mut("ipv4") { ipv4.insert("dns", Value::from(dns_u32)); @@ -672,9 +680,27 @@ mod tests { assert_eq!(shared["ipv4"].get("method"), Some(&Value::from("shared"))); } + /// Decodes NetworkManager's `ipv4.dns` payload back into addresses. + /// + /// Each entry is an `in_addr_t`, so the address octets are the `u32`'s + /// in-memory bytes. Deliberately expressed as the inverse of the contract + /// rather than by reusing the builder's own conversion. + fn decode_ipv4_dns(value: &Value<'_>) -> Vec { + >::try_from(value.try_clone().unwrap()) + .unwrap() + .into_iter() + .map(|raw| Ipv4Addr::from(raw.to_ne_bytes())) + .collect() + } + #[test] fn configures_ipv4_dns() { - let dns: Vec = vec!["8.8.8.8".parse().unwrap(), "1.1.1.1".parse().unwrap()]; + // Non-palindromic on purpose: `1.1.1.1`-style addresses are byte-order + // agnostic and hid the octet reversal in pop-os/cosmic-settings #2108. + let dns: Vec = vec![ + "10.2.0.1".parse().unwrap(), + "192.168.10.53".parse().unwrap(), + ]; let settings = ConnectionBuilder::new("802-3-ethernet", "eth0") .ipv4_auto() .ipv4_dns(dns.clone()) @@ -683,10 +709,24 @@ mod tests { let ipv4 = settings.get("ipv4").unwrap(); let value = ipv4.get("dns").unwrap(); assert_eq!(value.value_signature().to_string(), "au"); - assert_eq!( - value, - &Value::from(dns.into_iter().map(u32::from).collect::>()) - ); + assert_eq!(decode_ipv4_dns(value), dns); + } + + /// Regression test for pop-os/cosmic-settings #2108: `10.2.0.1` reached NetworkManager as + /// `1.0.2.10` because the builder sent the host-order integer. + #[test] + #[cfg(target_endian = "little")] + fn ipv4_dns_uses_network_byte_order() { + let settings = ConnectionBuilder::new("802-3-ethernet", "eth0") + .ipv4_auto() + .ipv4_dns(vec!["10.2.0.1".parse().unwrap()]) + .build(); + + let raw = >::try_from(settings["ipv4"].get("dns").unwrap().try_clone().unwrap()) + .unwrap(); + + assert_eq!(raw, vec![0x0100_020A]); + assert_ne!(raw, vec![u32::from(Ipv4Addr::new(10, 2, 0, 1))]); } #[test] @@ -825,7 +865,7 @@ mod tests { let settings = ConnectionBuilder::new("802-3-ethernet", "eth0") .ipv4_manual(vec![IpConfig::new("192.168.1.100", 24)]) .ipv4_gateway("192.168.1.1".parse().unwrap()) - .ipv4_dns(vec!["8.8.8.8".parse().unwrap()]) + .ipv4_dns(vec!["192.168.1.53".parse().unwrap()]) .build(); let ipv4 = settings.get("ipv4").unwrap(); @@ -836,8 +876,8 @@ mod tests { ); assert_eq!(ipv4.get("gateway"), Some(&Value::from("192.168.1.1"))); assert_eq!( - ipv4.get("dns"), - Some(&Value::from(vec![u32::from(Ipv4Addr::new(8, 8, 8, 8))])) + decode_ipv4_dns(ipv4.get("dns").unwrap()), + vec![Ipv4Addr::new(192, 168, 1, 53)] ); } } diff --git a/nmrs/src/api/builders/vpn.rs b/nmrs/src/api/builders/vpn.rs index 1c803ef1..d02cfabd 100644 --- a/nmrs/src/api/builders/vpn.rs +++ b/nmrs/src/api/builders/vpn.rs @@ -403,7 +403,8 @@ mod tests { "10.0.0.2/24", vec![peer], ) - .with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]) + // Non-palindromic on purpose: see #2108 on pop-os/cosmic-settings. + .with_dns(vec!["10.2.0.1".into(), "192.168.10.53".into()]) .with_mtu(1420) } @@ -636,12 +637,21 @@ mod tests { let settings = build_wireguard_connection(&creds, &opts).unwrap(); let ipv4 = settings.get("ipv4").unwrap(); + // NetworkManager reads each entry as an `in_addr_t`, so decode the + // native bytes back into an address rather than reusing the builder's + // own conversion. + let decoded: Vec = + >::try_from(ipv4.get("dns").unwrap().try_clone().unwrap()) + .unwrap() + .into_iter() + .map(|raw| std::net::Ipv4Addr::from(raw.to_ne_bytes())) + .collect(); assert_eq!( - ipv4.get("dns"), - Some(&Value::from(vec![ - u32::from(std::net::Ipv4Addr::new(1, 1, 1, 1)), - u32::from(std::net::Ipv4Addr::new(8, 8, 8, 8)), - ])) + decoded, + vec![ + std::net::Ipv4Addr::new(10, 2, 0, 1), + std::net::Ipv4Addr::new(192, 168, 10, 53), + ] ); assert_eq!(ipv4["dns"].value_signature().to_string(), "au"); } diff --git a/nmrs/src/api/builders/wireguard_builder.rs b/nmrs/src/api/builders/wireguard_builder.rs index b8930a06..fe6d3a07 100644 --- a/nmrs/src/api/builders/wireguard_builder.rs +++ b/nmrs/src/api/builders/wireguard_builder.rs @@ -602,19 +602,22 @@ mod tests { #[test] fn adds_dns_servers() { + // Non-palindromic IPv4 on purpose: see #2108 on pop-os/cosmic-settings. let settings = WireGuardBuilder::new("TestVPN") .private_key(PRIVATE_KEY) .address("10.0.0.2/24") .add_peer(create_test_peer()) - .dns(vec!["1.1.1.1".into(), "2001:4860:4860::8888".into()]) + .dns(vec!["10.2.0.1".into(), "2001:4860:4860::8888".into()]) .build() .expect("valid mixed-family DNS settings"); let ipv4 = settings.get("ipv4").unwrap(); - assert_eq!( - ipv4.get("dns"), - Some(&Value::from(vec![u32::from(Ipv4Addr::new(1, 1, 1, 1))])) - ); + let raw_v4 = >::try_from(ipv4.get("dns").unwrap().try_clone().unwrap()).unwrap(); + let decoded_v4: Vec = raw_v4 + .into_iter() + .map(|raw| Ipv4Addr::from(raw.to_ne_bytes())) + .collect(); + assert_eq!(decoded_v4, vec![Ipv4Addr::new(10, 2, 0, 1)]); assert_eq!(ipv4["dns"].value_signature().to_string(), "au"); let ipv6 = settings.get("ipv6").unwrap();