From fcb62e79a1e798a2f314fbf815f726c1365a1516 Mon Sep 17 00:00:00 2001
From: Benjamin <0xb35c@users.noreply.github.com>
Date: Wed, 18 Jan 2017 19:51:23 +0100
Subject: [PATCH] Fix issue #359: Bin to str conversion of IPv6 addresses
 (#431)

---
 scapy/pton_ntop.py  | 48 ++++++++++++++--------------
 test/regression.uts | 76 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 100 insertions(+), 24 deletions(-)

diff --git a/scapy/pton_ntop.py b/scapy/pton_ntop.py
index 25f7ef98..30512923 100644
--- a/scapy/pton_ntop.py
+++ b/scapy/pton_ntop.py
@@ -10,7 +10,10 @@ These functions are missing when python is compiled
 without IPv6 support, on Windows for instance.
 """
 
-import socket,struct
+import socket
+import re
+
+_IP6_ZEROS = re.compile('(?::|^)(0(?::0)+)(?::|$)')
 
 def inet_pton(af, addr):
     """Convert an IP address from text representation into binary form"""
@@ -75,27 +78,24 @@ def inet_ntop(af, addr):
         try:
             return socket.inet_ntop(af, addr)
         except AttributeError:
-            pass
-
-        # IPv6 addresses have 128bits (16 bytes)
-        if len(addr) != 16:
-            raise Exception("Illegal syntax for IP address")
-        parts = []
-        for left in [0, 2, 4, 6, 8, 10, 12, 14]:
-            try: 
-                value = struct.unpack("!H", addr[left:left+2])[0]
-                hexstr = hex(value)[2:]
-            except TypeError:
-                raise Exception("Illegal syntax for IP address")
-            parts.append(hexstr.lstrip("0").lower())
-        result = ":".join(parts)
-        while ":::" in result:
-            result = result.replace(":::", "::")
-        # Leaving out leading and trailing zeros is only allowed with ::
-        if result.endswith(":") and not result.endswith("::"):
-            result = result + "0"
-        if result.startswith(":") and not result.startswith("::"):
-            result = "0" + result
-        return result
+            return _ipv6_bin_to_str(addr)
     else:
-        raise Exception("Address family not supported yet")   
+        raise Exception("Address family not supported yet")
+
+
+def _ipv6_bin_to_str(addr):
+    # IPv6 addresses have 128bits (16 bytes)
+    if len(addr) != 16:
+        raise ValueError("invalid length of packed IP address string")
+
+    # Decode to hex representation
+    address = ":".join(addr[idx:idx + 2].encode('hex').lstrip('0') or '0' for idx in xrange(0, 16, 2))
+
+    try:
+        # Get the longest set of zero blocks
+        # Actually we need to take a look at group 1 regarding the length as 0:0:1:0:0:2:3:4 would have two matches:
+        # 0:0: and :0:0: where the latter is longer, though the first one should be taken. Group 1 is in both cases 0:0.
+        match = max(_IP6_ZEROS.finditer(address), key=lambda m: m.end(1) - m.start(1))
+        return '{}::{}'.format(address[:match.start()], address[match.end():])
+    except ValueError:
+        return address
diff --git a/test/regression.uts b/test/regression.uts
index 2ab4eeaf..de4d866a 100644
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -7047,3 +7047,79 @@ x = f.i2repr(mp, {'*', '+', 'bit 2'})
 assert(re.match(r'^.*Star \(\*\).*$', x) is not None)
 assert(re.match(r'^.*Plus \(\+\).*$', x) is not None)
 assert(re.match(r'^.*bit 2.*$', x) is not None)
+
+###########################################################################################################
++ Test correct conversion from binary to string of IPv6 addresses
+
+= IPv6 bin to string conversion - all zero bytes
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # All zero
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '::')
+
+= IPv6 bin to string conversion - non-compressable
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88' # Not compressable
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '1111:2222:3333:4444:5555:6666:7777:8888')
+
+= IPv6 bin to string conversion - Zero-block right
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x00\x00\x00\x00\x00\x00' # Zeroblock right
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '1111:2222:3333:4444:5555::')
+
+= IPv6 bin to string conversion - Zero-block left
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x00\x00\x00\x00\x00\x00\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88' # Zeroblock left
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '::4444:5555:6666:7777:8888')
+
+= IPv6 bin to string conversion - Two zero-block with different length
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x00\x00\x00\x00\x33\x33\x44\x44\x00\x00\x00\x00\x00\x00\x88\x88' # Short and long zero block
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '0:0:3333:4444::8888')
+
+= IPv6 bin to string conversion - Address 1::
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # only 1 on the left
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '1::')
+
+= IPv6 bin to string conversion - Address ::1
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' # loopback
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '::1')
+
+= IPv6 bin to string conversion - Zero-block of length 1
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x00\x00\x88\x88' # only one zero block
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == '1111:2222:3333:4444:5555:6666:0:8888')
+# On Mac OS socket.inet_ntop is not fully compliant with RFC 5952 and shortens the single zero block to '::'. Still
+# this is a valid IPv6 address representation.
+assert(compressed2 in ('1111:2222:3333:4444:5555:6666:0:8888', '1111:2222:3333:4444:5555:6666::8888'))
+
+= IPv6 bin to string conversion - Two zero-blocks with equal length
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x11\x11\x00\x00\x00\x00\x44\x44\x00\x00\x00\x00\x77\x77\x88\x88' # two zero blocks of equal length
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '1111::4444:0:0:7777:8888')
+
+= IPv6 bin to string conversion - Leading zero suppression
+from scapy.pton_ntop import _ipv6_bin_to_str, inet_ntop
+import socket
+address=b'\x10\x00\x02\x00\x00\x30\x00\x04\x00\x05\x00\x60\x07\x00\x80\x00' # Leading zero suppression
+compressed1, compressed2 = _ipv6_bin_to_str(address), inet_ntop(socket.AF_INET6, address)
+assert(compressed1 == compressed2 == '1000:200:30:4:5:60:700:8000')
-- 
GitLab