Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ 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

- Resolve Bluetooth devices through the BlueZ adapter that owns their address
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,
Expand Down Expand Up @@ -57,21 +60,26 @@ 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))
- `NetworkManager::add_connection()` and `NetworkManager::add_and_activate_connection()` for submitting builder output without custom zbus proxies ([#260](https://github.com/freedesktop-rs/nmrs/issues/260), [#465](https://github.com/freedesktop-rs/nmrs/pull/465))
- 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<MonitorHandle>` 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
Expand Down
60 changes: 50 additions & 10 deletions nmrs/src/api/builders/connection_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ipv4Addr>) -> Self {
let dns_u32: Vec<u32> = servers.into_iter().map(u32::from).collect();
let dns_u32: Vec<u32> = 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));
Expand Down Expand Up @@ -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<Ipv4Addr> {
<Vec<u32>>::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<Ipv4Addr> = 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<Ipv4Addr> = 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())
Expand All @@ -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::<Vec<_>>())
);
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 = <Vec<u32>>::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]
Expand Down Expand Up @@ -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();
Expand All @@ -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)]
);
}
}
22 changes: 16 additions & 6 deletions nmrs/src/api/builders/vpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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<std::net::Ipv4Addr> =
<Vec<u32>>::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");
}
Expand Down
13 changes: 8 additions & 5 deletions nmrs/src/api/builders/wireguard_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Vec<u32>>::try_from(ipv4.get("dns").unwrap().try_clone().unwrap()).unwrap();
let decoded_v4: Vec<Ipv4Addr> = 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();
Expand Down