Skip to content

bpo-34788: Add support for scoped IPv6 addresses (GH-13772) #273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Doc/library/ipaddress.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,20 @@ write code that handles both IP versions correctly. Address objects are
:RFC:`4291` for details. For example,
``"0000:0000:0000:0000:0000:0abc:0007:0def"`` can be compressed to
``"::abc:7:def"``.

Optionally, the string may also have a scope zone ID, expressed
with a suffix ``%scope_id``. If present, the scope ID must be non-empty,
and may not contain ``%``.
See :RFC:`4007` for details.
For example, ``fe80::1234%1`` might identify address ``fe80::1234`` on the first link of the node.
2. An integer that fits into 128 bits.
3. An integer packed into a :class:`bytes` object of length 16, big-endian.


>>> ipaddress.IPv6Address('2001:db8::1000')
IPv6Address('2001:db8::1000')
>>> ipaddress.IPv6Address('ff02::5678%1')
IPv6Address('ff02::5678%1')

.. attribute:: compressed

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

.. attribute:: scope_id

For scoped addresses as defined by :RFC:`4007`, this property identifies
the particular zone of the address's scope that the address belongs to,
as a string. When no scope zone is specified, this property will be ``None``.

.. attribute:: sixtofour

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

Note that IPv6 scoped addresses are converted to integers without scope zone ID.


Operators
^^^^^^^^^
Expand All @@ -311,15 +328,20 @@ IPv6).
Comparison operators
""""""""""""""""""""

Address objects can be compared with the usual set of comparison operators. Some
examples::
Address objects can be compared with the usual set of comparison operators.
Same IPv6 addresses with different scope zone IDs are not equal.
Some examples::

>>> IPv4Address('127.0.0.2') > IPv4Address('127.0.0.1')
True
>>> IPv4Address('127.0.0.2') == IPv4Address('127.0.0.1')
False
>>> IPv4Address('127.0.0.2') != IPv4Address('127.0.0.1')
True
>>> IPv6Address('fe80::1234') == IPv6Address('fe80::1234%1')
False
>>> IPv6Address('fe80::1234%1') != IPv6Address('fe80::1234%2')
True


Arithmetic operators
Expand Down
20 changes: 10 additions & 10 deletions Doc/library/socket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ created. Socket addresses are represented as follows:
Python programs.

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

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

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

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

.. versionchanged:: 3.7
for IPv6 multicast addresses, string representing an address will not
contain ``%scope`` part.
contain ``%scope_id`` part.

.. function:: getfqdn([name])

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

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

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

Expand Down Expand Up @@ -1354,7 +1354,7 @@ to sockets.

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

.. method:: socket.recvmsg(bufsize[, ancbufsize[, flags]])
Expand Down
2 changes: 2 additions & 0 deletions Doc/tools/susp-ignored.csv
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ library/ipaddress,,:db8,>>> ipaddress.IPv6Address('2001:db8::1000')
library/ipaddress,,::,>>> ipaddress.IPv6Address('2001:db8::1000')
library/ipaddress,,:db8,IPv6Address('2001:db8::1000')
library/ipaddress,,::,IPv6Address('2001:db8::1000')
library/ipaddress,,::,IPv6Address('ff02::5678%1')
library/ipaddress,,::,fe80::1234
library/ipaddress,,:db8,">>> ipaddress.ip_address(""2001:db8::1"").reverse_pointer"
library/ipaddress,,::,">>> ipaddress.ip_address(""2001:db8::1"").reverse_pointer"
library/ipaddress,,::,"""::abc:7:def"""
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ now raises :exc:`ImportError` instead of :exc:`ValueError` for invalid relative
import attempts.
(Contributed by Ngalim Siregar in :issue:`37444`.)

ipaddress
---------

:mod:`ipaddress` now supports IPv6 Scoped Addresses (IPv6 address with suffix ``%<scope_id>``).

Scoped IPv6 addresses can be parsed using :class:`ipaddress.IPv6Address`.
If present, scope zone ID is available through the :attr:`~ipaddress.IPv6Address.scope_id` attribute.
(Contributed by Oleksandr Pavliuk in :issue:`34788`.)

math
----

Expand Down
55 changes: 53 additions & 2 deletions Lib/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,26 @@ def _reverse_pointer(self):
reverse_chars = self.exploded[::-1].replace(':', '')
return '.'.join(reverse_chars) + '.ip6.arpa'

@staticmethod
def _split_scope_id(ip_str):
"""Helper function to parse IPv6 string address with scope id.

See RFC 4007 for details.

Args:
ip_str: A string, the IPv6 address.

Returns:
(addr, scope_id) tuple.

"""
addr, sep, scope_id = ip_str.partition('%')
if not sep:
scope_id = None
elif not scope_id or '%' in scope_id:
raise AddressValueError('Invalid IPv6 address: "%r"' % ip_str)
return addr, scope_id

@property
def max_prefixlen(self):
return self._max_prefixlen
Expand All @@ -1849,7 +1869,7 @@ class IPv6Address(_BaseV6, _BaseAddress):

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

__slots__ = ('_ip', '__weakref__')
__slots__ = ('_ip', '_scope_id', '__weakref__')

def __init__(self, address):
"""Instantiate a new IPv6 address object.
Expand All @@ -1872,21 +1892,52 @@ def __init__(self, address):
if isinstance(address, int):
self._check_int_address(address)
self._ip = address
self._scope_id = None
return

# Constructing from a packed address
if isinstance(address, bytes):
self._check_packed_address(address, 16)
self._ip = int.from_bytes(address, 'big')
self._scope_id = None
return

# Assume input argument to be string or any object representation
# which converts into a formatted IP string.
addr_str = str(address)
if '/' in addr_str:
raise AddressValueError("Unexpected '/' in %r" % address)
addr_str, self._scope_id = self._split_scope_id(addr_str)

self._ip = self._ip_int_from_string(addr_str)

def __str__(self):
ip_str = super().__str__()
return ip_str + '%' + self._scope_id if self._scope_id else ip_str

def __hash__(self):
return hash((self._ip, self._scope_id))

def __eq__(self, other):
address_equal = super().__eq__(other)
if address_equal is NotImplemented:
return NotImplemented
if not address_equal:
return False
return self._scope_id == getattr(other, '_scope_id', None)

@property
def scope_id(self):
"""Identifier of a particular zone of the address's scope.

See RFC 4007 for details.

Returns:
A string identifying the zone of the address if specified, else None.

"""
return self._scope_id

@property
def packed(self):
"""The binary representation of this address."""
Expand Down Expand Up @@ -2040,7 +2091,7 @@ def hostmask(self):
return self.network.hostmask

def __str__(self):
return '%s/%d' % (self._string_from_ip_int(self._ip),
return '%s/%d' % (super().__str__(),
self._prefixlen)

def __eq__(self, other):
Expand Down
Loading