summaryrefslogtreecommitdiffhomepage
path: root/libs/dns/ipv6.py
blob: 0db6fcfaa9763e12008b695d05768f26a442e859 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license

# Copyright (C) 2003-2017 Nominum, Inc.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose with or without fee is hereby granted,
# provided that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""IPv6 helper functions."""

import re
import binascii

import dns.exception
import dns.ipv4

_leading_zero = re.compile(r'0+([0-9a-f]+)')

def inet_ntoa(address):
    """Convert an IPv6 address in binary form to text form.

    *address*, a ``bytes``, the IPv6 address in binary form.

    Raises ``ValueError`` if the address isn't 16 bytes long.
    Returns a ``str``.
    """

    if len(address) != 16:
        raise ValueError("IPv6 addresses are 16 bytes long")
    hex = binascii.hexlify(address)
    chunks = []
    i = 0
    l = len(hex)
    while i < l:
        chunk = hex[i:i + 4].decode()
        # strip leading zeros.  we do this with an re instead of
        # with lstrip() because lstrip() didn't support chars until
        # python 2.2.2
        m = _leading_zero.match(chunk)
        if m is not None:
            chunk = m.group(1)
        chunks.append(chunk)
        i += 4
    #
    # Compress the longest subsequence of 0-value chunks to ::
    #
    best_start = 0
    best_len = 0
    start = -1
    last_was_zero = False
    for i in range(8):
        if chunks[i] != '0':
            if last_was_zero:
                end = i
                current_len = end - start
                if current_len > best_len:
                    best_start = start
                    best_len = current_len
                last_was_zero = False
        elif not last_was_zero:
            start = i
            last_was_zero = True
    if last_was_zero:
        end = 8
        current_len = end - start
        if current_len > best_len:
            best_start = start
            best_len = current_len
    if best_len > 1:
        if best_start == 0 and \
           (best_len == 6 or
            best_len == 5 and chunks[5] == 'ffff'):
            # We have an embedded IPv4 address
            if best_len == 6:
                prefix = '::'
            else:
                prefix = '::ffff:'
            hex = prefix + dns.ipv4.inet_ntoa(address[12:])
        else:
            hex = ':'.join(chunks[:best_start]) + '::' + \
                  ':'.join(chunks[best_start + best_len:])
    else:
        hex = ':'.join(chunks)
    return hex

_v4_ending = re.compile(br'(.*):(\d+\.\d+\.\d+\.\d+)$')
_colon_colon_start = re.compile(br'::.*')
_colon_colon_end = re.compile(br'.*::$')

def inet_aton(text, ignore_scope=False):
    """Convert an IPv6 address in text form to binary form.

    *text*, a ``str``, the IPv6 address in textual form.

    *ignore_scope*, a ``bool``.  If ``True``, a scope will be ignored.
    If ``False``, the default, it is an error for a scope to be present.

    Returns a ``bytes``.
    """

    #
    # Our aim here is not something fast; we just want something that works.
    #
    if not isinstance(text, bytes):
        text = text.encode()

    if ignore_scope:
        parts = text.split(b'%')
        l = len(parts)
        if l == 2:
            text = parts[0]
        elif l > 2:
            raise dns.exception.SyntaxError

    if text == b'':
        raise dns.exception.SyntaxError
    elif text.endswith(b':') and not text.endswith(b'::'):
        raise dns.exception.SyntaxError
    elif text.startswith(b':') and not text.startswith(b'::'):
        raise dns.exception.SyntaxError
    elif text == b'::':
        text = b'0::'
    #
    # Get rid of the icky dot-quad syntax if we have it.
    #
    m = _v4_ending.match(text)
    if m is not None:
        b = dns.ipv4.inet_aton(m.group(2))
        text = ("{}:{:02x}{:02x}:{:02x}{:02x}".format(m.group(1).decode(),
                                                      b[0], b[1], b[2],
                                                      b[3])).encode()
    #
    # Try to turn '::<whatever>' into ':<whatever>'; if no match try to
    # turn '<whatever>::' into '<whatever>:'
    #
    m = _colon_colon_start.match(text)
    if m is not None:
        text = text[1:]
    else:
        m = _colon_colon_end.match(text)
        if m is not None:
            text = text[:-1]
    #
    # Now canonicalize into 8 chunks of 4 hex digits each
    #
    chunks = text.split(b':')
    l = len(chunks)
    if l > 8:
        raise dns.exception.SyntaxError
    seen_empty = False
    canonical = []
    for c in chunks:
        if c == b'':
            if seen_empty:
                raise dns.exception.SyntaxError
            seen_empty = True
            for _ in range(0, 8 - l + 1):
                canonical.append(b'0000')
        else:
            lc = len(c)
            if lc > 4:
                raise dns.exception.SyntaxError
            if lc != 4:
                c = (b'0' * (4 - lc)) + c
            canonical.append(c)
    if l < 8 and not seen_empty:
        raise dns.exception.SyntaxError
    text = b''.join(canonical)

    #
    # Finally we can go to binary.
    #
    try:
        return binascii.unhexlify(text)
    except (binascii.Error, TypeError):
        raise dns.exception.SyntaxError

_mapped_prefix = b'\x00' * 10 + b'\xff\xff'

def is_mapped(address):
    """Is the specified address a mapped IPv4 address?

    *address*, a ``bytes`` is an IPv6 address in binary form.

    Returns a ``bool``.
    """

    return address.startswith(_mapped_prefix)