Implement overlaps() method for Ipv4Cidr and Ipv6Cidr to detect when address ranges overlap. This is important for preventing network conflicts when configuring SDN fabrics.
The implementation is quite simple: normalize the address using bitwise operations, then compare the prefix. Also add a few tests with edge-cases for IPv4 and IPv6. Signed-off-by: Gabriel Goller <g.gol...@proxmox.com> --- proxmox-network-types/src/ip_address.rs | 292 ++++++++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/proxmox-network-types/src/ip_address.rs b/proxmox-network-types/src/ip_address.rs index 8c21453a36b4..1c490ac30d76 100644 --- a/proxmox-network-types/src/ip_address.rs +++ b/proxmox-network-types/src/ip_address.rs @@ -321,6 +321,23 @@ impl Ipv4Cidr { pub fn mask(&self) -> u8 { self.mask } + + /// Checks if the two CIDRs overlap. + /// + /// CIDRs are always disjoint so we only need to check if one CIDR contains + /// the other. We do this by simply comparing the prefix. + 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()) + } } impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr { @@ -409,6 +426,23 @@ impl Ipv6Cidr { pub fn mask(&self) -> u8 { self.mask } + + /// Checks if the two CIDRs overlap. + /// + /// CIDRs are always disjoint so we only need to check if one CIDR contains + /// the other. We do this by simply comparing the prefix. + 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()) + } } impl std::str::FromStr for Ipv6Cidr { @@ -1569,4 +1603,262 @@ mod tests { range.to_cidrs().as_slice() ); } + + #[test] + fn test_ipv4_overlap() { + assert!( + Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24).unwrap()) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24).unwrap()) + ); + + assert!( + !Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.1.0".parse::<Ipv4Addr>().unwrap(), 24).unwrap()) + ); + + assert!( + Ipv4Cidr::new("192.168.0.200".parse::<Ipv4Addr>().unwrap(), 24) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.100".parse::<Ipv4Addr>().unwrap(), 24).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.128".parse::<Ipv4Addr>().unwrap(), 25).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 24) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.129".parse::<Ipv4Addr>().unwrap(), 25).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 16) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.129".parse::<Ipv4Addr>().unwrap(), 30).unwrap() + ) + ); + + assert!(Ipv4Cidr::new("10.0.0.1".parse::<Ipv4Addr>().unwrap(), 32) + .unwrap() + .overlaps(&Ipv4Cidr::new("10.0.0.1".parse::<Ipv4Addr>().unwrap(), 32).unwrap())); + + assert!(!Ipv4Cidr::new("10.0.0.1".parse::<Ipv4Addr>().unwrap(), 32) + .unwrap() + .overlaps(&Ipv4Cidr::new("10.0.0.2".parse::<Ipv4Addr>().unwrap(), 32).unwrap())); + + assert!(Ipv4Cidr::new("10.0.0.0".parse::<Ipv4Addr>().unwrap(), 8) + .unwrap() + .overlaps(&Ipv4Cidr::new("10.5.10.100".parse::<Ipv4Addr>().unwrap(), 32).unwrap())); + + assert!(Ipv4Cidr::new("0.0.0.0".parse::<Ipv4Addr>().unwrap(), 0) + .unwrap() + .overlaps(&Ipv4Cidr::new("172.16.0.0".parse::<Ipv4Addr>().unwrap(), 12).unwrap())); + + assert!( + !Ipv4Cidr::new("192.168.1.0".parse::<Ipv4Addr>().unwrap(), 30) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.1.4".parse::<Ipv4Addr>().unwrap(), 30).unwrap()) + ); + + assert!( + Ipv4Cidr::new("192.168.1.0".parse::<Ipv4Addr>().unwrap(), 30) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.1.2".parse::<Ipv4Addr>().unwrap(), 31).unwrap()) + ); + + assert!(!Ipv4Cidr::new("10.0.0.0".parse::<Ipv4Addr>().unwrap(), 8) + .unwrap() + .overlaps(&Ipv4Cidr::new("172.16.0.0".parse::<Ipv4Addr>().unwrap(), 12).unwrap())); + + assert!( + !Ipv4Cidr::new("172.16.0.0".parse::<Ipv4Addr>().unwrap(), 12) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 16).unwrap()) + ); + + assert!( + !Ipv4Cidr::new("192.168.0.0".parse::<Ipv4Addr>().unwrap(), 25) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.128".parse::<Ipv4Addr>().unwrap(), 25).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.64".parse::<Ipv4Addr>().unwrap(), 26) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.96".parse::<Ipv4Addr>().unwrap(), 27).unwrap()) + ); + + assert!( + !Ipv4Cidr::new("203.0.113.0".parse::<Ipv4Addr>().unwrap(), 31) + .unwrap() + .overlaps(&Ipv4Cidr::new("203.0.113.2".parse::<Ipv4Addr>().unwrap(), 31).unwrap()) + ); + + assert!(Ipv4Cidr::new("0.0.0.0".parse::<Ipv4Addr>().unwrap(), 0) + .unwrap() + .overlaps(&Ipv4Cidr::new("0.0.0.0".parse::<Ipv4Addr>().unwrap(), 0).unwrap())); + + assert!( + Ipv4Cidr::new("255.255.255.255".parse::<Ipv4Addr>().unwrap(), 0) + .unwrap() + .overlaps(&Ipv4Cidr::new("0.0.0.0".parse::<Ipv4Addr>().unwrap(), 32).unwrap()) + ); + + assert!( + Ipv4Cidr::new("255.255.255.255".parse::<Ipv4Addr>().unwrap(), 0) + .unwrap() + .overlaps( + &Ipv4Cidr::new("255.255.255.255".parse::<Ipv4Addr>().unwrap(), 0).unwrap() + ) + ); + } + + #[test] + fn test_ipv6_overlap() { + assert!( + Ipv6Cidr::new("2001:db8::0".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8::127".parse::<Ipv6Addr>().unwrap(), 64).unwrap() + ) + ); + + assert!( + !Ipv6Cidr::new("2001:db8:abc:1234::1".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:abc:1235::1".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8:abc:1235::1".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:abc:1235::7".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8::200".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8::100".parse::<Ipv6Addr>().unwrap(), 70).unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8::1".parse::<Ipv6Addr>().unwrap(), 128) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::1".parse::<Ipv6Addr>().unwrap(), 128).unwrap()) + ); + assert!( + !Ipv6Cidr::new("2001:db8::1".parse::<Ipv6Addr>().unwrap(), 128) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::2".parse::<Ipv6Addr>().unwrap(), 128).unwrap()) + ); + + assert!(Ipv6Cidr::new("2001:db8::".parse::<Ipv6Addr>().unwrap(), 32) + .unwrap() + .overlaps( + &Ipv6Cidr::new( + "2001:db8:cafe:babe::dead:beef".parse::<Ipv6Addr>().unwrap(), + 128 + ) + .unwrap() + )); + + assert!(Ipv6Cidr::new("::0".parse::<Ipv6Addr>().unwrap(), 0) + .unwrap() + .overlaps(&Ipv6Cidr::new("fe80::".parse::<Ipv6Addr>().unwrap(), 10).unwrap())); + + assert!(!Ipv6Cidr::new("fe80::".parse::<Ipv6Addr>().unwrap(), 10) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::".parse::<Ipv6Addr>().unwrap(), 32).unwrap())); + + assert!(!Ipv6Cidr::new("fc00::".parse::<Ipv6Addr>().unwrap(), 7) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::".parse::<Ipv6Addr>().unwrap(), 32).unwrap())); + + assert!(Ipv6Cidr::new("2001:db8::".parse::<Ipv6Addr>().unwrap(), 16) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:1234:5678::abcd".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + )); + + assert!( + !Ipv6Cidr::new("2001:db8:0000::".parse::<Ipv6Addr>().unwrap(), 48) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:0001::".parse::<Ipv6Addr>().unwrap(), 48).unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8:1234::".parse::<Ipv6Addr>().unwrap(), 48) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:1234:5678::".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + ) + ); + + assert!( + !Ipv6Cidr::new("2001:db8::0".parse::<Ipv6Addr>().unwrap(), 127) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::2".parse::<Ipv6Addr>().unwrap(), 127).unwrap()) + ); + + assert!( + Ipv6Cidr::new("2001:db8::0".parse::<Ipv6Addr>().unwrap(), 127) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::1".parse::<Ipv6Addr>().unwrap(), 127).unwrap()) + ); + + assert!( + !Ipv6Cidr::new("2001:db8::0".parse::<Ipv6Addr>().unwrap(), 126) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::4".parse::<Ipv6Addr>().unwrap(), 126).unwrap()) + ); + assert!( + Ipv6Cidr::new("2001:db8::0".parse::<Ipv6Addr>().unwrap(), 126) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::2".parse::<Ipv6Addr>().unwrap(), 127).unwrap()) + ); + + assert!( + Ipv6Cidr::new("2001:db8:1::".parse::<Ipv6Addr>().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new( + "2001:db8:1:0:ebcd:eebf::efee".parse::<Ipv6Addr>().unwrap(), + 80 + ) + .unwrap() + ) + ); + } } -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel