Skip to content
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

bpo-37228: Fix loop.create_datagram_endpoint()'s usage of SO_REUSEADDR #17311

Merged
@@ -473,6 +473,21 @@ Opening network connections
reuse_address=None, reuse_port=None, \
allow_broadcast=None, sock=None)

.. note::
The parameter *reuse_address* is no longer supported, as using
:py:data:`~sockets.SO_REUSEADDR` poses a significant security concern for
This conversation was marked as resolved by aeros

This comment has been minimized.

@aeros

aeros Nov 22, 2019
Author Member

Is it adequate to have "significant security concern" or should we explicitly explain the vulnerability? I left it out because I think it might encourage potential exploitation on systems that haven't updated to the latest release of their respective version (such as a system on Python 3.5.9 or lower), which I'd rather avoid doing.

This comment has been minimized.

@pitrou

pitrou Nov 25, 2019
Member

I think the vulnerability should be explicitly described. Perhaps not in depth, but at least let the reader understand.

This comment has been minimized.

@aeros

aeros Nov 25, 2019
Author Member

@pitrou

I think the vulnerability should be explicitly described.

Done. Let me know if the new description is sufficient.

UDP. Explicitly passing ``reuse_address=True`` will raise an exception.

When multiple processes with differing UIDs assign sockets to an
indentical UDP socket address with ``SO_REUSEADDR``, incoming packets can
become randomly distributed among the sockets.

For supported platforms, *reuse_port* can be used as a replacement for
similar functionality. With *reuse_port*,
:py:data:`~sockets.SO_REUSEPORT` is used instead, which specifically
prevents processes with differing UIDs from assigning sockets to the same
socket address.

Create a datagram connection.

The socket family can be either :py:data:`~socket.AF_INET`,
@@ -501,11 +516,6 @@ Opening network connections
resolution. If given, these should all be integers from the
corresponding :mod:`socket` module constants.

* *reuse_address* tells the kernel to reuse a local socket in
``TIME_WAIT`` state, without waiting for its natural timeout to
expire. If not specified will automatically be set to ``True`` on
Unix.

* *reuse_port* tells the kernel to allow this endpoint to be bound to the
same port as other existing endpoints are bound to, so long as they all
set this flag when being created. This option is not supported on Windows
@@ -527,6 +537,10 @@ Opening network connections
The *family*, *proto*, *flags*, *reuse_address*, *reuse_port,
*allow_broadcast*, and *sock* parameters were added.

.. versionchanged:: 3.8.1
The *reuse_address* parameter is no longer supported due to security
concerns.

.. versionchanged:: 3.8
Added support for Windows.

@@ -66,6 +66,10 @@
# Maximum timeout passed to select to avoid OS limitations
MAXIMUM_SELECT_TIMEOUT = 24 * 3600

# Used for deprecation and removal of `loop.create_datagram_endpoint()`'s
# *reuse_address* parameter
_unset = object()


def _format_handle(handle):
cb = handle._callback
@@ -1230,7 +1234,7 @@ async def start_tls(self, transport, protocol, sslcontext, *,
async def create_datagram_endpoint(self, protocol_factory,
local_addr=None, remote_addr=None, *,
family=0, proto=0, flags=0,
reuse_address=None, reuse_port=None,
reuse_address=_unset, reuse_port=None,
allow_broadcast=None, sock=None):
"""Create datagram connection."""
if sock is not None:
@@ -1239,7 +1243,7 @@ async def create_datagram_endpoint(self, protocol_factory,
f'A UDP Socket was expected, got {sock!r}')
if (local_addr or remote_addr or
family or proto or flags or
reuse_address or reuse_port or allow_broadcast):
reuse_port or allow_broadcast):
# show the problematic kwargs in exception msg
opts = dict(local_addr=local_addr, remote_addr=remote_addr,
family=family, proto=proto, flags=flags,
@@ -1306,8 +1310,18 @@ async def create_datagram_endpoint(self, protocol_factory,

exceptions = []

if reuse_address is None:
reuse_address = os.name == 'posix' and sys.platform != 'cygwin'
This conversation was marked as resolved by aeros
Comment on lines -1309 to -1310

This comment has been minimized.

@aeros

aeros Nov 21, 2019
Author Member

Should we have a similar default for reuse_port or should users have to explicitly specify it?

if reuse_port is None:
    reuse_port = hasattr(sockets, 'SO_REUSEPORT')
...

Personally, I'm in favor of having reuse_port as the default on supported platforms, but either would be fine.

# bpo-37228
if reuse_address is not _unset:
if reuse_address:
raise ValueError("Passing `reuse_address=True` is no "
"longer supported, as the usage of "
"SO_REUSEPORT in UDP poses a significant "
"security concern.")
else:
warnings.warn("The *reuse_address* parameter has been "
"deprecated as of 3.5.10 and is scheduled "
"for removal in 3.11.", DeprecationWarning,
stacklevel=2)

for ((family, proto),
(local_address, remote_address)) in addr_pairs_info:
@@ -1316,9 +1330,6 @@ async def create_datagram_endpoint(self, protocol_factory,
try:
sock = socket.socket(
family=family, type=socket.SOCK_DGRAM, proto=proto)
if reuse_address:
sock.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if reuse_port:
_set_reuseport(sock)
if allow_broadcast:
@@ -1738,10 +1738,6 @@ class FakeSock:
MyDatagramProto, flags=1, sock=FakeSock())
self.assertRaises(ValueError, self.loop.run_until_complete, fut)

fut = self.loop.create_datagram_endpoint(
MyDatagramProto, reuse_address=True, sock=FakeSock())
self.assertRaises(ValueError, self.loop.run_until_complete, fut)

fut = self.loop.create_datagram_endpoint(
MyDatagramProto, reuse_port=True, sock=FakeSock())
self.assertRaises(ValueError, self.loop.run_until_complete, fut)
@@ -1752,7 +1748,6 @@ class FakeSock:

def test_create_datagram_endpoint_sockopts(self):
# Socket options should not be applied unless asked for.
# SO_REUSEADDR defaults to on for UNIX.
# SO_REUSEPORT is not available on all platforms.

coro = self.loop.create_datagram_endpoint(
@@ -1761,18 +1756,8 @@ def test_create_datagram_endpoint_sockopts(self):
transport, protocol = self.loop.run_until_complete(coro)
sock = transport.get_extra_info('socket')

reuse_address_default_on = (
os.name == 'posix' and sys.platform != 'cygwin')
reuseport_supported = hasattr(socket, 'SO_REUSEPORT')

if reuse_address_default_on:
self.assertTrue(
sock.getsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR))
else:
self.assertFalse(
sock.getsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR))
if reuseport_supported:
self.assertFalse(
sock.getsockopt(
@@ -1788,13 +1773,12 @@ def test_create_datagram_endpoint_sockopts(self):
coro = self.loop.create_datagram_endpoint(
lambda: MyDatagramProto(create_future=True, loop=self.loop),
local_addr=('127.0.0.1', 0),
reuse_address=True,
reuse_port=reuseport_supported,
allow_broadcast=True)
transport, protocol = self.loop.run_until_complete(coro)
sock = transport.get_extra_info('socket')

self.assertTrue(
self.assertFalse(
sock.getsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR))
if reuseport_supported:
@@ -1809,6 +1793,29 @@ def test_create_datagram_endpoint_sockopts(self):
self.loop.run_until_complete(protocol.done)
self.assertEqual('CLOSED', protocol.state)

def test_create_datagram_endpoint_reuse_address_error(self):
# bpo-37228: Ensure that explicit passing of `reuse_address=True`
# raises an error, as it is not safe to use SO_REUSEADDR when using UDP

coro = self.loop.create_datagram_endpoint(
lambda: MyDatagramProto(create_future=True, loop=self.loop),
local_addr=('127.0.0.1', 0),
reuse_address=True)

with self.assertRaises(ValueError):
self.loop.run_until_complete(coro)

def test_create_datagram_endpoint_reuse_address_warning(self):
# bpo-37228: Deprecate *reuse_address* parameter

coro = self.loop.create_datagram_endpoint(
lambda: MyDatagramProto(create_future=True, loop=self.loop),
local_addr=('127.0.0.1', 0),
reuse_address=False)

with self.assertWarns(DeprecationWarning):
self.loop.run_until_complete(coro)

@patch_socket
def test_create_datagram_endpoint_nosoreuseport(self, m_socket):
del m_socket.SO_REUSEPORT
@@ -0,0 +1,6 @@
Due to significant security concerns, the *reuse_address* parameter of
:meth:`asyncio.loop.create_datagram_endpoint` is no longer supported. This is
because of the behavior of ``SO_REUSEADDR`` in UDP. For more details, see the
documentation for ``loop.create_datagram_endpoint()``.
(Contributed by Kyle Stanley, Antoine Pitrou, and Yury Selivanov in
:issue:`37228`.)