Repository: netbox-community/netbox

Issue:
Non-pool IPv6 prefixes think the 0th address is available
### Environment
* Python version:  3.6.7
* NetBox version:  2.7.7

### Steps to Reproduce
1. Create an IPv6 prefix (tested /64) that is not configured as a pool
2. Add an address to the prefix (::1 is the simplest test case)
3. Visit the ip-addresses view for this prefix

<!-- What did you expect to happen? -->
### Expected Behavior
The count of available IPs 'below' the first allocated IP should not include the 0th address, unless the prefix is configured as a pool (not sure address pools make sense in IPv6 context at all, tbh) or is a /127.

<!-- What happened instead? -->
### Observed Behavior
The 0th IP is counted as 'available' in the address list. For example, allocating the ::1 address shows a list entry with '1 IP Available' below the ::1 address. The count will be off-by-one for other cases where there are actually available IPs. See screenshot.

Clicking 'Add an IP address' chooses ::2 as the default as expected, and the `available-ips` API call also correctly does not include the 0th address.

![image](https://user-images.githubusercontent.com/1471533/75191973-3b08aa80-5708-11ea-965b-b9b0e38bfd8e.png)

Non-pool IPv6 prefixes think the 0th address is available
### Environment
* Python version:  3.6.7
* NetBox version:  2.7.7

### Steps to Reproduce
1. Create an IPv6 prefix (tested /64) that is not configured as a pool
2. Add an address to the prefix (::1 is the simplest test case)
3. Visit the ip-addresses view for this prefix

<!-- What did you expect to happen? -->
### Expected Behavior
The count of available IPs 'below' the first allocated IP should not include the 0th address, unless the prefix is configured as a pool (not sure address pools make sense in IPv6 context at all, tbh) or is a /127.

<!-- What happened instead? -->
### Observed Behavior
The 0th IP is counted as 'available' in the address list. For example, allocating the ::1 address shows a list entry with '1 IP Available' below the ::1 address. The count will be off-by-one for other cases where there are actually available IPs. See screenshot.

Clicking 'Add an IP address' chooses ::2 as the default as expected, and the `available-ips` API call also correctly does not include the 0th address.

![image](https://user-images.githubusercontent.com/1471533/75191973-3b08aa80-5708-11ea-965b-b9b0e38bfd8e.png)



Discussion:
The all 0's address is indeed available in IPv6. Same goes for the all 1's address. From [RFC 4291](https://tools.ietf.org/html/rfc4291#page-3) (at the end of section 2):

> In IPv6, all zeros and all ones are legal values for any field, unless specifically excluded. Specifically, prefixes may contain, or end with, zero-valued fields.
> The all 0's address is indeed available in IPv6. Same goes for the all 1's address. From [RFC 4291](https://tools.ietf.org/html/rfc4291#page-3) (at the end of section 2):
> 
> > In IPv6, all zeros and all ones are legal values for any field, unless specifically excluded. Specifically, prefixes may contain, or end with, zero-valued fields.

It's a valid address, but it is reserved for Subnet-Router anycast (see section 2.6.1), so I don't think it makes sense for it to be assignable within Netbox.

Though if there's opposition to this, I suppose that the API behaviour should probably be consistent.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).

This issue has been automatically closed due to lack of activity. In an effort to reduce noise, please do not comment any further. Note that the core maintainers may elect to reopen this issue at a later date if deemed necessary.

I'm re-opening this issue because I am having this issue when sending an API request to NetBox, requesting NetBox to hand out available IP addresses within an IPv6 prefix. (names and prefixes have been sanitized to protect the innocent)

```
changed: [switch.example.com] => (item=2001:DB8:50::/80) => {"ansible_loop_var": "item", "changed": true, "ip_address": {"address": "2001:DB8:50::/80", "assigned_object": 192293, "assigned_object_id": 192293, "assigned_object_type": "dcim.interface", "created": "2022-04-11", "custom_fields": {}, "description": "", "display": "2001:DB8:50::/80", "dns_name": "", "family": 6, "id": 31556, "last_updated": "2022-04-11T15:04:10.275113Z", "nat_inside": null, "nat_outside": null, "role": null, "status": "active", "tags": [], "tenant": null, "url": "https://netbox.example.com/api/ipam/ip-addresses/31556/", "vrf": null}, "item": "2001:DB8:50::/80", "msg": "ip_address 2001:DB8:50::/80 created"}
```

```
#switch.example.com(conf-if-lo-0)# ipv6 address 2001:DB8:50::/128
% Error: Invalid IPV6 address used as global unicast address
```
I also agree with @ktims assessment that the ::0'th IP address is reserved for subnet-router anycast
I believe the culprit is the logic at https://github.com/netbox-community/netbox/blob/develop/netbox/ipam/models/ip.py#L510-L512
The all 0's address is indeed available in IPv6. Same goes for the all 1's address. From [RFC 4291](https://tools.ietf.org/html/rfc4291#page-3) (at the end of section 2):

> In IPv6, all zeros and all ones are legal values for any field, unless specifically excluded. Specifically, prefixes may contain, or end with, zero-valued fields.
> The all 0's address is indeed available in IPv6. Same goes for the all 1's address. From [RFC 4291](https://tools.ietf.org/html/rfc4291#page-3) (at the end of section 2):
> 
> > In IPv6, all zeros and all ones are legal values for any field, unless specifically excluded. Specifically, prefixes may contain, or end with, zero-valued fields.

It's a valid address, but it is reserved for Subnet-Router anycast (see section 2.6.1), so I don't think it makes sense for it to be assignable within Netbox.

Though if there's opposition to this, I suppose that the API behaviour should probably be consistent.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).

This issue has been automatically closed due to lack of activity. In an effort to reduce noise, please do not comment any further. Note that the core maintainers may elect to reopen this issue at a later date if deemed necessary.

I'm re-opening this issue because I am having this issue when sending an API request to NetBox, requesting NetBox to hand out available IP addresses within an IPv6 prefix. (names and prefixes have been sanitized to protect the innocent)

```
changed: [switch.example.com] => (item=2001:DB8:50::/80) => {"ansible_loop_var": "item", "changed": true, "ip_address": {"address": "2001:DB8:50::/80", "assigned_object": 192293, "assigned_object_id": 192293, "assigned_object_type": "dcim.interface", "created": "2022-04-11", "custom_fields": {}, "description": "", "display": "2001:DB8:50::/80", "dns_name": "", "family": 6, "id": 31556, "last_updated": "2022-04-11T15:04:10.275113Z", "nat_inside": null, "nat_outside": null, "role": null, "status": "active", "tags": [], "tenant": null, "url": "https://netbox.example.com/api/ipam/ip-addresses/31556/", "vrf": null}, "item": "2001:DB8:50::/80", "msg": "ip_address 2001:DB8:50::/80 created"}
```

```
#switch.example.com(conf-if-lo-0)# ipv6 address 2001:DB8:50::/128
% Error: Invalid IPV6 address used as global unicast address
```
I also agree with @ktims assessment that the ::0'th IP address is reserved for subnet-router anycast
I believe the culprit is the logic at https://github.com/netbox-community/netbox/blob/develop/netbox/ipam/models/ip.py#L510-L512

Resolving patch:
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -507,16 +507,20 @@ def get_available_ips(self):
             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):


Test patch:
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -185,6 +185,18 @@ def test_get_first_available_ip(self):
         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),