Repository: nautobot/nautobot

Issue:
`::1/128` is not a valid prefix
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.6
* Nautobot version: 1.1.3

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->

When trying to create the prefix `::1/128` I get the following error:

```no-highlight
<class 'netaddr.core.AddrFormatError'>

invalid IPNetwork 0.0.0.1/128
```

Both Python netaddr and ipaddress modules see this as a valid IPNetwork.  

### Steps to Reproduce
1.  Create a prefix or aggregate using the prefix `::1/128`

<!-- What did you expect to happen? -->
### Expected Behavior

Prefix created

<!-- What happened instead? -->
### Observed Behavior

```
invalid IPNetwork 0.0.0.1/128
```


Discussion:
I dug into this and I don't know the solution, but what I can tell is that this is due to the `VarBinaryIPField` and how it gets stored in the DB as well as how it is extracted from the DB back into Python.

It will store the value in the DB for the IPv6 address as `b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'` which just equates to 1. There is no logic in `VarbinaryIPField` to know what family it needs to cast the binary to.

I get it to work for prefixes by adding the following to the `cidr_str` method on the `Prefix` model.

```python

    @property
    def cidr_str(self):
        if self.prefix_length is not None and self.prefix_length > 32 and "." in self.network:
            ip_address = netaddr.IPAddress(self.network)
            return "%s/%s" % (ip_address.ipv6(True), self.prefix_length)
        if self.network is not None and self.prefix_length is not None:
            return "%s/%s" % (self.network, self.prefix_length)
```

This resolves the whole loading bug that we were getting, but the utilization is now 100% which is different than the `0.0.0.1/32` that I have for a test for ipv4.

It also still renders `prefix.network` as `0.0.0.1/32` when accessing the attribute.

```python
>>> from nautobot.ipam.models import Prefix
>>> p = Prefix.objects.first()
>>> p
<Prefix: ::1/128>
>>> p.prefix.size
1
>>> p.get_child_ips()
<IPAddressQuerySet [<IPAddress: 0.0.0.1/32>]>
>>> Prefix.STATUS_CONTAINER
<Status: Container>
>>> p.vrf
>>> from nautobot.ipam.models import IPAddress
>>> p.prefix
IPNetwork('::1/128')
>>> IPAddress.objects.net_host_contained(p.prefix).filter(vrf=None)
<IPAddressQuerySet [<IPAddress: 0.0.0.1/32>]>
>>> p.prefix.broadcast
IPAddress('::1')
>>> p.network
'0.0.0.1'
>>> p.prefix.network
```

Hopefully this helps a little bit, but I don't know the best way to handle this in the long run. It would be great if we could determine within the `VarbinaryIPField` which family the IP should exist to (seems like it will most likely need to be via the `prefix_length` of the `Prefix` model.
Thanks for the report and the investigation! I'm looking into this as well.

Resolving patch:
diff --git a/nautobot/ipam/fields.py b/nautobot/ipam/fields.py
--- a/nautobot/ipam/fields.py
+++ b/nautobot/ipam/fields.py
@@ -39,12 +39,17 @@ def _parse_address(self, value):
         Parse `str`, `bytes` (varbinary), or `netaddr.IPAddress to `netaddr.IPAddress`.
         """
         try:
-            value = int.from_bytes(value, "big")
+            int_value = int.from_bytes(value, "big")
+            # Distinguish between
+            # \x00\x00\x00\x01 (IPv4 0.0.0.1) and
+            # \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 (IPv6 ::1), among other cases
+            version = 4 if len(value) == 4 else 6
+            value = int_value
         except TypeError:
-            pass  # It's a string
+            version = None  # It's a string, IP version should be self-evident
 
         try:
-            return netaddr.IPAddress(value)
+            return netaddr.IPAddress(value, version=version)
         except netaddr.AddrFormatError:
             raise ValidationError("Invalid IP address format: {}".format(value))
         except (TypeError, ValueError) as e:


Test patch:
diff --git a/nautobot/ipam/tests/test_models.py b/nautobot/ipam/tests/test_models.py
--- a/nautobot/ipam/tests/test_models.py
+++ b/nautobot/ipam/tests/test_models.py
@@ -55,6 +55,13 @@ def test_parse_address_success(self):
         # IPAddress => netaddr.IPAddress
         self.assertEqual(self.field._parse_address(obj), obj)
 
+        # Special cases involving values that could be IPv4 or IPv6 if naively interpreted
+        self.assertEqual(self.field._parse_address(bytes(netaddr.IPAddress("0.0.0.1"))), netaddr.IPAddress("0.0.0.1"))
+        self.assertEqual(self.field._parse_address(bytes(netaddr.IPAddress("::1"))), netaddr.IPAddress("::1"))
+        self.assertEqual(
+            self.field._parse_address(bytes(netaddr.IPAddress("::192.0.2.15"))), netaddr.IPAddress("::192.0.2.15")
+        )
+
     def test_parse_address_failure(self):
         """"Test `VarbinaryIPField._parse_address` FAIL."""