Skip to content

Commit fa4c7c7

Browse files
Bump min version for ula fn + mcast admin check (#50)
Includes: - Bump Rust min-version to 1.84 for direct `is_unique_local` call on IPv6 addrs - Add `is_admin_scoped_multicast` check for mcast IPv6 addrs that are site-local or org scoped - [gh-actions] msrv update - [release] with bump, update to 0.1.2 oxnet - [review] add admin-local scope from RFC 7346
1 parent 22273e5 commit fa4c7c7

File tree

5 files changed

+94
-10
lines changed

5 files changed

+94
-10
lines changed

.github/workflows/rust.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ jobs:
2626
strategy:
2727
matrix:
2828
os: [ubuntu-latest, windows-latest, macos-latest]
29-
# 1.78 is the MSRV
30-
rust-version: [ stable, "1.78" ]
31-
features: [ all, default ]
29+
# 1.84 is the MSRV
30+
rust-version: [stable, "1.84"]
31+
features: [all, default]
3232
include:
3333
- features: all
3434
feature_flags: --all-features

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.1.2] - 2025-05-25
4+
5+
* Bumps Rust min-version to 1.84 for direct `is_unique_local` call on IPv6
6+
addresses.
7+
* Adds `is_admin_scoped_multicast` check for multicast IPv6 addresses that are
8+
site-local or org scoped
9+
310
## [0.1.1] - 2025-02-25
411

512
* Adds `is_subnet_of`/`is_supernet_of`/`overlaps`, for verifying disjoint
@@ -12,5 +19,6 @@
1219

1320
Initial release.
1421

22+
[0.1.2]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.2
1523
[0.1.1]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.1
1624
[0.1.0]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.0

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[package]
22
name = "oxnet"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
edition = "2021"
5-
rust-version = "1.78.0"
5+
rust-version = "1.84.0"
66
license = "MIT OR Apache-2.0"
77
description = "commonly used networking primitives with common traits implemented"
88
readme = "README.md"

src/ipnet.rs

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,28 @@ impl IpNet {
138138
}
139139
}
140140

141+
/// Return `true` iff this subnet is in a multicast address range with
142+
/// administrative scope (admin-local, site-local or organization-local) as
143+
/// defined in [RFC 7346] and [RFC 4291].
144+
///
145+
/// [RFC 7346]: https://tools.ietf.org/html/rfc7346
146+
/// [RFC 4291]: https://tools.ietf.org/html/rfc4291
147+
pub const fn is_admin_scoped_multicast(&self) -> bool {
148+
match self {
149+
IpNet::V4(_inner) => false, // IPv4 does not support ULA
150+
IpNet::V6(inner) => inner.is_admin_scoped_multicast(),
151+
}
152+
}
153+
154+
/// Return `true` iff this subnet is in a Unique Local Address range.
155+
/// This is only valid for IPv6 addresses.
156+
pub const fn is_unique_local(&self) -> bool {
157+
match self {
158+
IpNet::V4(_inner) => false, // IPv4 does not support ULA
159+
IpNet::V6(inner) => inner.is_unique_local(),
160+
}
161+
}
162+
141163
/// Return `true` iff this subnet is in a loopback address range.
142164
pub const fn is_loopback(&self) -> bool {
143165
match self {
@@ -606,6 +628,26 @@ impl Ipv6Net {
606628
self.addr.is_multicast()
607629
}
608630

631+
/// Return `true` if this address is a multicast address with
632+
/// administrative scope (admin-local, site-local or organization-local) as
633+
/// defined in [RFC 7346] and [RFC 4291].
634+
///
635+
/// [RFC 7346]: https://tools.ietf.org/html/rfc7346
636+
/// [RFC 4291]: https://tools.ietf.org/html/rfc4291
637+
pub const fn is_admin_scoped_multicast(&self) -> bool {
638+
if !self.addr.is_multicast() {
639+
return false;
640+
}
641+
642+
// Extract the scope field (bits 4-7 of the second byte)
643+
let segments = self.addr.segments();
644+
let scope = (segments[0] & 0x000F) as u8;
645+
646+
// RFC 4291/7346: Scope values 4 (admin-local), 5 (site-local) and
647+
// 8 (organization-local)
648+
matches!(scope, 0x4 | 0x5 | 0x8)
649+
}
650+
609651
/// Return `true` iff this subnet is in a loopback address range.
610652
pub const fn is_loopback(&self) -> bool {
611653
self.addr.is_loopback()
@@ -624,10 +666,11 @@ impl Ipv6Net {
624666
}
625667

626668
/// Return `true` if this subnetwork is in the IPv6 Unique Local Address
627-
/// range defined in RFC 4193, e.g., `fd00:/8`
628-
pub fn is_unique_local(&self) -> bool {
629-
// TODO: Delegate to `Ipv6Addr::is_unique_local()` when stabilized.
630-
self.first_addr().octets()[0] == 0xfd
669+
/// range defined in [RFC 4193], e.g., `fd00:/8`.
670+
///
671+
/// [RFC 4193]: https://tools.ietf.org/html/rfc4193
672+
pub const fn is_unique_local(&self) -> bool {
673+
self.addr.is_unique_local()
631674
}
632675

633676
/// Return the first address within this subnet.
@@ -1076,4 +1119,37 @@ mod tests {
10761119
let unspec: IpNet = "0.0.0.0/0".parse().unwrap();
10771120
assert!(unspec.is_network_address());
10781121
}
1122+
1123+
#[test]
1124+
fn test_is_multicast_with_scopes() {
1125+
// IPv4 multicast tests (224.0.0.0/4 is the IPv4 multicast range)
1126+
let v4_mcast: IpNet = "224.0.0.1/32".parse().unwrap();
1127+
let v4_not_mcast: IpNet = "192.168.1.1/24".parse().unwrap();
1128+
1129+
assert!(v4_mcast.is_multicast());
1130+
assert!(!v4_not_mcast.is_multicast());
1131+
1132+
// IPv6 multicast tests (ff00::/8 is the IPv6 multicast range)
1133+
let v6_mcast: IpNet = "ff02::1/128".parse().unwrap();
1134+
let v6_not_mcast: IpNet = "2001:db8::1/64".parse().unwrap();
1135+
1136+
assert!(v6_mcast.is_multicast());
1137+
assert!(!v6_not_mcast.is_multicast());
1138+
1139+
// Test for multicast_admin_scoped (site-local)
1140+
let v6_site_scoped_mcast: IpNet = "ff05::1/128".parse().unwrap();
1141+
// Test for multicast_admin_scoped (organization-local)
1142+
let v6_org_scoped_mcast: IpNet = "ff08::1/128".parse().unwrap();
1143+
//Test for multicast_admin_scoped (admin-local)
1144+
let v6_admin_scoped_mcast: IpNet = "ff04::1/128".parse().unwrap();
1145+
// Test for a multicast address that is not admin scoped
1146+
let v6_not_admin_scoped_mcast: IpNet = "ff02::1/128".parse().unwrap();
1147+
1148+
assert!(v6_site_scoped_mcast.is_admin_scoped_multicast());
1149+
assert!(v6_org_scoped_mcast.is_admin_scoped_multicast());
1150+
assert!(v6_admin_scoped_mcast.is_admin_scoped_multicast());
1151+
assert!(!v6_not_admin_scoped_mcast.is_admin_scoped_multicast());
1152+
// Always false for IPv4
1153+
assert!(!v4_mcast.is_admin_scoped_multicast());
1154+
}
10791155
}

0 commit comments

Comments
 (0)