Skip to content

Commit 21da76d

Browse files
authored
bpo-34788: Add support for scoped IPv6 addresses (GH-13772)
Automerge-Triggered-By: @asvetlov
1 parent be7ead6 commit 21da76d

File tree

7 files changed

+544
-37
lines changed

7 files changed

+544
-37
lines changed

Doc/library/ipaddress.rst

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,20 @@ write code that handles both IP versions correctly. Address objects are
217217
:RFC:`4291` for details. For example,
218218
``"0000:0000:0000:0000:0000:0abc:0007:0def"`` can be compressed to
219219
``"::abc:7:def"``.
220+
221+
Optionally, the string may also have a scope zone ID, expressed
222+
with a suffix ``%scope_id``. If present, the scope ID must be non-empty,
223+
and may not contain ``%``.
224+
See :RFC:`4007` for details.
225+
For example, ``fe80::1234%1`` might identify address ``fe80::1234`` on the first link of the node.
220226
2. An integer that fits into 128 bits.
221227
3. An integer packed into a :class:`bytes` object of length 16, big-endian.
222228

229+
223230
>>> ipaddress.IPv6Address('2001:db8::1000')
224231
IPv6Address('2001:db8::1000')
232+
>>> ipaddress.IPv6Address('ff02::5678%1')
233+
IPv6Address('ff02::5678%1')
225234

226235
.. attribute:: compressed
227236

@@ -268,6 +277,12 @@ write code that handles both IP versions correctly. Address objects are
268277
``::FFFF/96``), this property will report the embedded IPv4 address.
269278
For any other address, this property will be ``None``.
270279

280+
.. attribute:: scope_id
281+
282+
For scoped addresses as defined by :RFC:`4007`, this property identifies
283+
the particular zone of the address's scope that the address belongs to,
284+
as a string. When no scope zone is specified, this property will be ``None``.
285+
271286
.. attribute:: sixtofour
272287

273288
For addresses that appear to be 6to4 addresses (starting with
@@ -299,6 +314,8 @@ the :func:`str` and :func:`int` builtin functions::
299314
>>> int(ipaddress.IPv6Address('::1'))
300315
1
301316

317+
Note that IPv6 scoped addresses are converted to integers without scope zone ID.
318+
302319

303320
Operators
304321
^^^^^^^^^
@@ -311,15 +328,20 @@ IPv6).
311328
Comparison operators
312329
""""""""""""""""""""
313330

314-
Address objects can be compared with the usual set of comparison operators. Some
315-
examples::
331+
Address objects can be compared with the usual set of comparison operators.
332+
Same IPv6 addresses with different scope zone IDs are not equal.
333+
Some examples::
316334

317335
>>> IPv4Address('127.0.0.2') > IPv4Address('127.0.0.1')
318336
True
319337
>>> IPv4Address('127.0.0.2') == IPv4Address('127.0.0.1')
320338
False
321339
>>> IPv4Address('127.0.0.2') != IPv4Address('127.0.0.1')
322340
True
341+
>>> IPv6Address('fe80::1234') == IPv6Address('fe80::1234%1')
342+
False
343+
>>> IPv6Address('fe80::1234%1') != IPv6Address('fe80::1234%2')
344+
True
323345

324346

325347
Arithmetic operators

Doc/library/socket.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ created. Socket addresses are represented as follows:
7878
Python programs.
7979

8080
- For :const:`AF_INET6` address family, a four-tuple ``(host, port, flowinfo,
81-
scopeid)`` is used, where *flowinfo* and *scopeid* represent the ``sin6_flowinfo``
81+
scope_id)`` is used, where *flowinfo* and *scope_id* represent the ``sin6_flowinfo``
8282
and ``sin6_scope_id`` members in :const:`struct sockaddr_in6` in C. For
83-
:mod:`socket` module methods, *flowinfo* and *scopeid* can be omitted just for
84-
backward compatibility. Note, however, omission of *scopeid* can cause problems
83+
:mod:`socket` module methods, *flowinfo* and *scope_id* can be omitted just for
84+
backward compatibility. Note, however, omission of *scope_id* can cause problems
8585
in manipulating scoped IPv6 addresses.
8686

8787
.. versionchanged:: 3.7
88-
For multicast addresses (with *scopeid* meaningful) *address* may not contain
89-
``%scope`` (or ``zone id``) part. This information is superfluous and may
88+
For multicast addresses (with *scope_id* meaningful) *address* may not contain
89+
``%scope_id`` (or ``zone id``) part. This information is superfluous and may
9090
be safely omitted (recommended).
9191

9292
- :const:`AF_NETLINK` sockets are represented as pairs ``(pid, groups)``.
@@ -738,7 +738,7 @@ The :mod:`socket` module also offers various network-related services:
738738
:const:`AI_CANONNAME` is part of the *flags* argument; else *canonname*
739739
will be empty. *sockaddr* is a tuple describing a socket address, whose
740740
format depends on the returned *family* (a ``(address, port)`` 2-tuple for
741-
:const:`AF_INET`, a ``(address, port, flow info, scope id)`` 4-tuple for
741+
:const:`AF_INET`, a ``(address, port, flowinfo, scope_id)`` 4-tuple for
742742
:const:`AF_INET6`), and is meant to be passed to the :meth:`socket.connect`
743743
method.
744744

@@ -759,7 +759,7 @@ The :mod:`socket` module also offers various network-related services:
759759

760760
.. versionchanged:: 3.7
761761
for IPv6 multicast addresses, string representing an address will not
762-
contain ``%scope`` part.
762+
contain ``%scope_id`` part.
763763

764764
.. function:: getfqdn([name])
765765

@@ -827,8 +827,8 @@ The :mod:`socket` module also offers various network-related services:
827827
or numeric address representation in *host*. Similarly, *port* can contain a
828828
string port name or a numeric port number.
829829

830-
For IPv6 addresses, ``%scope`` is appended to the host part if *sockaddr*
831-
contains meaningful *scopeid*. Usually this happens for multicast addresses.
830+
For IPv6 addresses, ``%scope_id`` is appended to the host part if *sockaddr*
831+
contains meaningful *scope_id*. Usually this happens for multicast addresses.
832832

833833
For more information about *flags* you can consult :manpage:`getnameinfo(3)`.
834834

@@ -1354,7 +1354,7 @@ to sockets.
13541354

13551355
.. versionchanged:: 3.7
13561356
For multicast IPv6 address, first item of *address* does not contain
1357-
``%scope`` part anymore. In order to get full IPv6 address use
1357+
``%scope_id`` part anymore. In order to get full IPv6 address use
13581358
:func:`getnameinfo`.
13591359

13601360
.. method:: socket.recvmsg(bufsize[, ancbufsize[, flags]])

Doc/tools/susp-ignored.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ library/ipaddress,,:db8,>>> ipaddress.IPv6Address('2001:db8::1000')
147147
library/ipaddress,,::,>>> ipaddress.IPv6Address('2001:db8::1000')
148148
library/ipaddress,,:db8,IPv6Address('2001:db8::1000')
149149
library/ipaddress,,::,IPv6Address('2001:db8::1000')
150+
library/ipaddress,,::,IPv6Address('ff02::5678%1')
151+
library/ipaddress,,::,fe80::1234
150152
library/ipaddress,,:db8,">>> ipaddress.ip_address(""2001:db8::1"").reverse_pointer"
151153
library/ipaddress,,::,">>> ipaddress.ip_address(""2001:db8::1"").reverse_pointer"
152154
library/ipaddress,,::,"""::abc:7:def"""

Doc/whatsnew/3.9.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,15 @@ now raises :exc:`ImportError` instead of :exc:`ValueError` for invalid relative
213213
import attempts.
214214
(Contributed by Ngalim Siregar in :issue:`37444`.)
215215

216+
ipaddress
217+
---------
218+
219+
:mod:`ipaddress` now supports IPv6 Scoped Addresses (IPv6 address with suffix ``%<scope_id>``).
220+
221+
Scoped IPv6 addresses can be parsed using :class:`ipaddress.IPv6Address`.
222+
If present, scope zone ID is available through the :attr:`~ipaddress.IPv6Address.scope_id` attribute.
223+
(Contributed by Oleksandr Pavliuk in :issue:`34788`.)
224+
216225
math
217226
----
218227

Lib/ipaddress.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,6 +1836,26 @@ def _reverse_pointer(self):
18361836
reverse_chars = self.exploded[::-1].replace(':', '')
18371837
return '.'.join(reverse_chars) + '.ip6.arpa'
18381838

1839+
@staticmethod
1840+
def _split_scope_id(ip_str):
1841+
"""Helper function to parse IPv6 string address with scope id.
1842+
1843+
See RFC 4007 for details.
1844+
1845+
Args:
1846+
ip_str: A string, the IPv6 address.
1847+
1848+
Returns:
1849+
(addr, scope_id) tuple.
1850+
1851+
"""
1852+
addr, sep, scope_id = ip_str.partition('%')
1853+
if not sep:
1854+
scope_id = None
1855+
elif not scope_id or '%' in scope_id:
1856+
raise AddressValueError('Invalid IPv6 address: "%r"' % ip_str)
1857+
return addr, scope_id
1858+
18391859
@property
18401860
def max_prefixlen(self):
18411861
return self._max_prefixlen
@@ -1849,7 +1869,7 @@ class IPv6Address(_BaseV6, _BaseAddress):
18491869

18501870
"""Represent and manipulate single IPv6 Addresses."""
18511871

1852-
__slots__ = ('_ip', '__weakref__')
1872+
__slots__ = ('_ip', '_scope_id', '__weakref__')
18531873

18541874
def __init__(self, address):
18551875
"""Instantiate a new IPv6 address object.
@@ -1872,21 +1892,52 @@ def __init__(self, address):
18721892
if isinstance(address, int):
18731893
self._check_int_address(address)
18741894
self._ip = address
1895+
self._scope_id = None
18751896
return
18761897

18771898
# Constructing from a packed address
18781899
if isinstance(address, bytes):
18791900
self._check_packed_address(address, 16)
18801901
self._ip = int.from_bytes(address, 'big')
1902+
self._scope_id = None
18811903
return
18821904

18831905
# Assume input argument to be string or any object representation
18841906
# which converts into a formatted IP string.
18851907
addr_str = str(address)
18861908
if '/' in addr_str:
18871909
raise AddressValueError("Unexpected '/' in %r" % address)
1910+
addr_str, self._scope_id = self._split_scope_id(addr_str)
1911+
18881912
self._ip = self._ip_int_from_string(addr_str)
18891913

1914+
def __str__(self):
1915+
ip_str = super().__str__()
1916+
return ip_str + '%' + self._scope_id if self._scope_id else ip_str
1917+
1918+
def __hash__(self):
1919+
return hash((self._ip, self._scope_id))
1920+
1921+
def __eq__(self, other):
1922+
address_equal = super().__eq__(other)
1923+
if address_equal is NotImplemented:
1924+
return NotImplemented
1925+
if not address_equal:
1926+
return False
1927+
return self._scope_id == getattr(other, '_scope_id', None)
1928+
1929+
@property
1930+
def scope_id(self):
1931+
"""Identifier of a particular zone of the address's scope.
1932+
1933+
See RFC 4007 for details.
1934+
1935+
Returns:
1936+
A string identifying the zone of the address if specified, else None.
1937+
1938+
"""
1939+
return self._scope_id
1940+
18901941
@property
18911942
def packed(self):
18921943
"""The binary representation of this address."""
@@ -2040,7 +2091,7 @@ def hostmask(self):
20402091
return self.network.hostmask
20412092

20422093
def __str__(self):
2043-
return '%s/%d' % (self._string_from_ip_int(self._ip),
2094+
return '%s/%d' % (super().__str__(),
20442095
self._prefixlen)
20452096

20462097
def __eq__(self, other):

0 commit comments

Comments
 (0)