From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox 1/1] network-types: add method to canonicalize IPv4 and IPv6 CIDRs
Date: Wed, 23 Jul 2025 16:31:59 +0200 [thread overview]
Message-ID: <20250723143200.737707-1-g.goller@proxmox.com> (raw)
When a cidr address in a FRR access-list is not canonicalized (i.e. is
not a network address) then we get a warning in the journal and
frr-reload.py will even fail. So we need to convert the address entered
by the user into a network address. Factor out the already existing
helper and add a new method to do this. Also add some unit tests.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-network-types/src/ip_address.rs | 209 ++++++++++++++++++++++--
1 file changed, 195 insertions(+), 14 deletions(-)
diff --git a/proxmox-network-types/src/ip_address.rs b/proxmox-network-types/src/ip_address.rs
index b91ede445070..1c72197534ce 100644
--- a/proxmox-network-types/src/ip_address.rs
+++ b/proxmox-network-types/src/ip_address.rs
@@ -322,6 +322,15 @@ impl Ipv4Cidr {
self.mask
}
+ /// Get the canonical representation of a IPv4 CIDR address.
+ ///
+ /// This normalizes the address, so we get the first address of a CIDR subnet (e.g.
+ /// 2.2.2.200/24 -> 2.2.2.0) we do this by using a bitwise AND operation over the address and
+ /// the u32::MAX (all ones) shifted by the mask.
+ fn normalize(addr: u32, mask: u8) -> u32 {
+ addr & u32::MAX.checked_shl((32 - mask).into()).unwrap_or(0)
+ }
+
/// Checks if the two CIDRs overlap.
///
/// CIDRs are always disjoint so we only need to check if one CIDR contains
@@ -329,14 +338,20 @@ impl Ipv4Cidr {
pub fn overlaps(&self, other: &Ipv4Cidr) -> bool {
// we normalize by the smallest mask, so the larger of the two subnets.
let min_mask = self.mask().min(other.mask());
- // this normalizes the address, so we get the first address of a CIDR
- // (e.g. 2.2.2.200/24 -> 2.2.2.0) we do this by using a bitwise AND
- // operation over the address and the u32::MAX (all ones) shifted by
- // the mask.
- let normalize =
- |addr: u32| addr & u32::MAX.checked_shl((32 - min_mask).into()).unwrap_or(0);
// if the prefix is the same we have an overlap
- normalize(self.address().to_bits()) == normalize(other.address().to_bits())
+ Self::normalize(self.address().to_bits(), min_mask)
+ == Self::normalize(other.address().to_bits(), min_mask)
+ }
+
+ /// Get the canonical version of the CIDR.
+ ///
+ /// A canonicalized CIDR is a the normalized address, so the first address in the subnet
+ /// (sometimes also called "network address"). E.g. 2.2.2.5/24 -> 2.2.2.0/24
+ pub fn canonical(&self) -> Self {
+ Self {
+ addr: Ipv4Addr::from_bits(Self::normalize(self.addr.to_bits(), self.mask())),
+ mask: self.mask(),
+ }
}
}
@@ -424,6 +439,15 @@ impl Ipv6Cidr {
self.mask
}
+ /// Get the canonical representation of a IPv6 CIDR address.
+ ///
+ /// This normalizes the address, so we get the first address of a CIDR subnet (e.g.
+ /// 2001:db8::4/64 -> 2001:db8::0/64) we do this by using a bitwise AND operation over the address and
+ /// the u128::MAX (all ones) shifted by the mask.
+ fn normalize(addr: u128, mask: u8) -> u128 {
+ addr & u128::MAX.checked_shl((128 - mask).into()).unwrap_or(0)
+ }
+
/// Checks if the two CIDRs overlap.
///
/// CIDRs are always disjoint so we only need to check if one CIDR contains
@@ -431,14 +455,20 @@ impl Ipv6Cidr {
pub fn overlaps(&self, other: &Ipv6Cidr) -> bool {
// we normalize by the smallest mask, so the larger of the two subnets.
let min_mask = self.mask().min(other.mask());
- // this normalizes the address, so we get the first address of a CIDR
- // (e.g. 2001:db8::200/64 -> 2001:db8::0) we do this by using a bitwise AND
- // operation over the address and the u128::MAX (all ones) shifted by
- // the mask.
- let normalize =
- |addr: u128| addr & u128::MAX.checked_shl((128 - min_mask).into()).unwrap_or(0);
// if the prefix is the same we have an overlap
- normalize(self.address().to_bits()) == normalize(other.address().to_bits())
+ Self::normalize(self.address().to_bits(), min_mask)
+ == Self::normalize(other.address().to_bits(), min_mask)
+ }
+
+ /// Get the canonical version of the CIDR.
+ ///
+ /// A canonicalized CIDR is a the normalized address, so the first address in the subnet
+ /// (sometimes also called "network address"). E.g. 2001:db8::5/64 -> 2001:db8::0/64
+ pub fn canonical(&self) -> Self {
+ Self {
+ addr: Ipv6Addr::from_bits(Self::normalize(self.addr.to_bits(), self.mask())),
+ mask: self.mask(),
+ }
}
}
@@ -1855,4 +1885,155 @@ mod tests {
)
);
}
+
+ #[test]
+ fn test_ipv4_canonical() {
+ let cidr = Ipv4Cidr::new("192.168.1.100".parse::<Ipv4Addr>().unwrap(), 24).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(192, 168, 1, 0));
+ assert_eq!(canonical.mask, 24);
+
+ let cidr = Ipv4Cidr::new("10.50.75.200".parse::<Ipv4Addr>().unwrap(), 16).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(10, 50, 0, 0));
+ assert_eq!(canonical.mask, 16);
+
+ let cidr = Ipv4Cidr::new("172.16.100.50".parse::<Ipv4Addr>().unwrap(), 8).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(172, 0, 0, 0));
+ assert_eq!(canonical.mask, 8);
+
+ let cidr = Ipv4Cidr::new("192.168.1.1".parse::<Ipv4Addr>().unwrap(), 32).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(192, 168, 1, 1));
+ assert_eq!(canonical.mask, 32);
+
+ let cidr = Ipv4Cidr::new("255.255.255.255".parse::<Ipv4Addr>().unwrap(), 0).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(0, 0, 0, 0));
+ assert_eq!(canonical.mask, 0);
+
+ let cidr = Ipv4Cidr::new("192.168.1.103".parse::<Ipv4Addr>().unwrap(), 30).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(192, 168, 1, 100));
+ assert_eq!(canonical.mask, 30);
+
+ let cidr = Ipv4Cidr::new("10.10.15.128".parse::<Ipv4Addr>().unwrap(), 23).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv4Addr::new(10, 10, 14, 0));
+ assert_eq!(canonical.mask, 23);
+
+ let cidr = Ipv4Cidr::new("203.0.113.99".parse::<Ipv4Addr>().unwrap(), 25).unwrap();
+ let canonical1 = cidr.canonical();
+ let canonical2 = canonical1.canonical();
+ assert_eq!(canonical1.addr, canonical2.addr);
+ assert_eq!(canonical1.mask, canonical2.mask);
+ }
+
+ #[test]
+ fn test_ipv6_canonical() {
+ let cidr = Ipv6Cidr::new(
+ "2001:db8:85a3::8a2e:370:7334".parse::<Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(
+ canonical.addr,
+ Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0, 0, 0, 0, 0)
+ );
+ assert_eq!(canonical.mask, 64);
+
+ let cidr = Ipv6Cidr::new(
+ "2001:db8:1234:5678:9abc:def0:1234:5678"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ 48,
+ )
+ .unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(
+ canonical.addr,
+ Ipv6Addr::new(0x2001, 0xdb8, 0x1234, 0, 0, 0, 0, 0)
+ );
+ assert_eq!(canonical.mask, 48);
+
+ let cidr = Ipv6Cidr::new(
+ "2001:db8:abcd:ef01:2345:6789:abcd:ef01"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ 32,
+ )
+ .unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(
+ canonical.addr,
+ Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)
+ );
+ assert_eq!(canonical.mask, 32);
+
+ let cidr = Ipv6Cidr::new("2001:db8::1".parse::<Ipv6Addr>().unwrap(), 128).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(
+ canonical.addr,
+ Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)
+ );
+ assert_eq!(canonical.mask, 128);
+
+ let cidr = Ipv6Cidr::new(
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ 0,
+ )
+ .unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0));
+ assert_eq!(canonical.mask, 0);
+
+ let cidr = Ipv6Cidr::new(
+ "2001:db8:1234:5600:ffff:ffff:ffff:ffff"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ 56,
+ )
+ .unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(
+ canonical.addr,
+ Ipv6Addr::new(0x2001, 0xdb8, 0x1234, 0x5600, 0, 0, 0, 0)
+ );
+ assert_eq!(canonical.mask, 56);
+
+ let cidr = Ipv6Cidr::new(
+ "2001:db8:1234:5678:9abc:def0:ffff:ffff"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ 96,
+ )
+ .unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(
+ canonical.addr,
+ Ipv6Addr::new(0x2001, 0xdb8, 0x1234, 0x5678, 0x9abc, 0xdef0, 0, 0)
+ );
+ assert_eq!(canonical.mask, 96);
+
+ let cidr = Ipv6Cidr::new(
+ "2001:db8:cafe:face:dead:beef:1234:5678"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ 80,
+ )
+ .unwrap();
+ let canonical1 = cidr.canonical();
+ let canonical2 = canonical1.canonical();
+ assert_eq!(canonical1.addr, canonical2.addr);
+ assert_eq!(canonical1.mask, canonical2.mask);
+
+ let cidr = Ipv6Cidr::new("fe80::1".parse::<Ipv6Addr>().unwrap(), 64).unwrap();
+ let canonical = cidr.canonical();
+ assert_eq!(canonical.addr, Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0));
+ assert_eq!(canonical.mask, 64);
+ }
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next reply other threads:[~2025-07-23 14:31 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-07-23 14:31 Gabriel Goller [this message]
2025-07-23 14:32 ` [pve-devel] [PATCH proxmox-ve-rs 1/1] ve-config: fabrics: force ip-prefix to be canonical Gabriel Goller
2025-07-30 12:00 ` [pve-devel] applied: [PATCH proxmox 1/1] network-types: add method to canonicalize IPv4 and IPv6 CIDRs Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250723143200.737707-1-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.