From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 75D341FF183 for ; Wed, 2 Jul 2025 16:53:46 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 24B841ED71; Wed, 2 Jul 2025 16:51:50 +0200 (CEST) From: Gabriel Goller To: pve-devel@lists.proxmox.com Date: Wed, 2 Jul 2025 16:49:49 +0200 Message-Id: <20250702145101.894299-5-g.goller@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250702145101.894299-1-g.goller@proxmox.com> References: <20250702145101.894299-1-g.goller@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.019 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH proxmox v4 4/5] network-types: add CIDR overlap detection for IPv4 and IPv6 X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" 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 --- 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> From 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::().unwrap(), 24) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 24).unwrap()) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 24) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 24).unwrap()) + ); + + assert!( + !Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 24) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.1.0".parse::().unwrap(), 24).unwrap()) + ); + + assert!( + Ipv4Cidr::new("192.168.0.200".parse::().unwrap(), 24) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.100".parse::().unwrap(), 24).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 24) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.128".parse::().unwrap(), 25).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 24) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.129".parse::().unwrap(), 25).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 16) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.129".parse::().unwrap(), 30).unwrap() + ) + ); + + assert!(Ipv4Cidr::new("10.0.0.1".parse::().unwrap(), 32) + .unwrap() + .overlaps(&Ipv4Cidr::new("10.0.0.1".parse::().unwrap(), 32).unwrap())); + + assert!(!Ipv4Cidr::new("10.0.0.1".parse::().unwrap(), 32) + .unwrap() + .overlaps(&Ipv4Cidr::new("10.0.0.2".parse::().unwrap(), 32).unwrap())); + + assert!(Ipv4Cidr::new("10.0.0.0".parse::().unwrap(), 8) + .unwrap() + .overlaps(&Ipv4Cidr::new("10.5.10.100".parse::().unwrap(), 32).unwrap())); + + assert!(Ipv4Cidr::new("0.0.0.0".parse::().unwrap(), 0) + .unwrap() + .overlaps(&Ipv4Cidr::new("172.16.0.0".parse::().unwrap(), 12).unwrap())); + + assert!( + !Ipv4Cidr::new("192.168.1.0".parse::().unwrap(), 30) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.1.4".parse::().unwrap(), 30).unwrap()) + ); + + assert!( + Ipv4Cidr::new("192.168.1.0".parse::().unwrap(), 30) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.1.2".parse::().unwrap(), 31).unwrap()) + ); + + assert!(!Ipv4Cidr::new("10.0.0.0".parse::().unwrap(), 8) + .unwrap() + .overlaps(&Ipv4Cidr::new("172.16.0.0".parse::().unwrap(), 12).unwrap())); + + assert!( + !Ipv4Cidr::new("172.16.0.0".parse::().unwrap(), 12) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 16).unwrap()) + ); + + assert!( + !Ipv4Cidr::new("192.168.0.0".parse::().unwrap(), 25) + .unwrap() + .overlaps( + &Ipv4Cidr::new("192.168.0.128".parse::().unwrap(), 25).unwrap() + ) + ); + + assert!( + Ipv4Cidr::new("192.168.0.64".parse::().unwrap(), 26) + .unwrap() + .overlaps(&Ipv4Cidr::new("192.168.0.96".parse::().unwrap(), 27).unwrap()) + ); + + assert!( + !Ipv4Cidr::new("203.0.113.0".parse::().unwrap(), 31) + .unwrap() + .overlaps(&Ipv4Cidr::new("203.0.113.2".parse::().unwrap(), 31).unwrap()) + ); + + assert!(Ipv4Cidr::new("0.0.0.0".parse::().unwrap(), 0) + .unwrap() + .overlaps(&Ipv4Cidr::new("0.0.0.0".parse::().unwrap(), 0).unwrap())); + + assert!( + Ipv4Cidr::new("255.255.255.255".parse::().unwrap(), 0) + .unwrap() + .overlaps(&Ipv4Cidr::new("0.0.0.0".parse::().unwrap(), 32).unwrap()) + ); + + assert!( + Ipv4Cidr::new("255.255.255.255".parse::().unwrap(), 0) + .unwrap() + .overlaps( + &Ipv4Cidr::new("255.255.255.255".parse::().unwrap(), 0).unwrap() + ) + ); + } + + #[test] + fn test_ipv6_overlap() { + assert!( + Ipv6Cidr::new("2001:db8::0".parse::().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8::127".parse::().unwrap(), 64).unwrap() + ) + ); + + assert!( + !Ipv6Cidr::new("2001:db8:abc:1234::1".parse::().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:abc:1235::1".parse::().unwrap(), 64) + .unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8:abc:1235::1".parse::().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:abc:1235::7".parse::().unwrap(), 64) + .unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8::200".parse::().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8::100".parse::().unwrap(), 70).unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8::1".parse::().unwrap(), 128) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::1".parse::().unwrap(), 128).unwrap()) + ); + assert!( + !Ipv6Cidr::new("2001:db8::1".parse::().unwrap(), 128) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::2".parse::().unwrap(), 128).unwrap()) + ); + + assert!(Ipv6Cidr::new("2001:db8::".parse::().unwrap(), 32) + .unwrap() + .overlaps( + &Ipv6Cidr::new( + "2001:db8:cafe:babe::dead:beef".parse::().unwrap(), + 128 + ) + .unwrap() + )); + + assert!(Ipv6Cidr::new("::0".parse::().unwrap(), 0) + .unwrap() + .overlaps(&Ipv6Cidr::new("fe80::".parse::().unwrap(), 10).unwrap())); + + assert!(!Ipv6Cidr::new("fe80::".parse::().unwrap(), 10) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::".parse::().unwrap(), 32).unwrap())); + + assert!(!Ipv6Cidr::new("fc00::".parse::().unwrap(), 7) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::".parse::().unwrap(), 32).unwrap())); + + assert!(Ipv6Cidr::new("2001:db8::".parse::().unwrap(), 16) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:1234:5678::abcd".parse::().unwrap(), 64) + .unwrap() + )); + + assert!( + !Ipv6Cidr::new("2001:db8:0000::".parse::().unwrap(), 48) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:0001::".parse::().unwrap(), 48).unwrap() + ) + ); + + assert!( + Ipv6Cidr::new("2001:db8:1234::".parse::().unwrap(), 48) + .unwrap() + .overlaps( + &Ipv6Cidr::new("2001:db8:1234:5678::".parse::().unwrap(), 64) + .unwrap() + ) + ); + + assert!( + !Ipv6Cidr::new("2001:db8::0".parse::().unwrap(), 127) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::2".parse::().unwrap(), 127).unwrap()) + ); + + assert!( + Ipv6Cidr::new("2001:db8::0".parse::().unwrap(), 127) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::1".parse::().unwrap(), 127).unwrap()) + ); + + assert!( + !Ipv6Cidr::new("2001:db8::0".parse::().unwrap(), 126) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::4".parse::().unwrap(), 126).unwrap()) + ); + assert!( + Ipv6Cidr::new("2001:db8::0".parse::().unwrap(), 126) + .unwrap() + .overlaps(&Ipv6Cidr::new("2001:db8::2".parse::().unwrap(), 127).unwrap()) + ); + + assert!( + Ipv6Cidr::new("2001:db8:1::".parse::().unwrap(), 64) + .unwrap() + .overlaps( + &Ipv6Cidr::new( + "2001:db8:1:0:ebcd:eebf::efee".parse::().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