diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 9aec0cff8..a3b8fb2c1 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -507,16 +507,20 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel): child_ranges.add(iprange.range) available_ips = prefix - child_ips - child_ranges - # IPv6, pool, or IPv4 /31-/32 sets are fully usable - if self.family == 6 or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31): + # IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable + if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31): return available_ips - # For "normal" IPv4 prefixes, omit first and last addresses - available_ips -= netaddr.IPSet([ - netaddr.IPAddress(self.prefix.first), - netaddr.IPAddress(self.prefix.last), - ]) - + if self.family == 4: + # For "normal" IPv4 prefixes, omit first and last addresses + available_ips -= netaddr.IPSet([ + netaddr.IPAddress(self.prefix.first), + netaddr.IPAddress(self.prefix.last), + ]) + else: + # For IPv6 prefixes, omit the Subnet-Router anycast address + # per RFC 4291 + available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)]) return available_ips def get_first_available_ip(self): diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index a664b34f4..09bc95799 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -185,6 +185,18 @@ class TestPrefix(TestCase): IPAddress.objects.create(address=IPNetwork('10.0.0.4/24')) self.assertEqual(parent_prefix.get_first_available_ip(), '10.0.0.5/24') + def test_get_first_available_ip_ipv6(self): + parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500::/64')) + self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500::1/64') + + def test_get_first_available_ip_ipv6_rfc3627(self): + parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500:4::/126')) + self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500:4::1/126') + + def test_get_first_available_ip_ipv6_rfc6164(self): + parent_prefix = Prefix.objects.create(prefix=IPNetwork('2001:db8:500:5::/127')) + self.assertEqual(parent_prefix.get_first_available_ip(), '2001:db8:500:5::/127') + def test_get_utilization_container(self): prefixes = ( Prefix(prefix=IPNetwork('10.0.0.0/24'), status=PrefixStatusChoices.STATUS_CONTAINER),