diff --git a/eventlet/convenience.py b/eventlet/convenience.py index 88343a9..560de23 100644 --- a/eventlet/convenience.py +++ b/eventlet/convenience.py @@ -1,6 +1,6 @@ import sys +import warnings -from eventlet import greenio from eventlet import greenpool from eventlet import greenthread from eventlet.green import socket @@ -22,7 +22,11 @@ def connect(addr, family=socket.AF_INET, bind=None): return sock -def listen(addr, family=socket.AF_INET, backlog=50): +class ReuseRandomPortWarning(Warning): + pass + + +def listen(addr, family=socket.AF_INET, backlog=50, reuse_addr=True, reuse_port=None): """Convenience function for opening server sockets. This socket can be used in :func:`~eventlet.serve` or a custom ``accept()`` loop. @@ -38,9 +42,18 @@ def listen(addr, family=socket.AF_INET, backlog=50): :return: The listening green socket object. """ sock = socket.socket(family, socket.SOCK_STREAM) - if sys.platform[:3] != "win": + if reuse_addr and sys.platform[:3] != 'win': sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if hasattr(socket, 'SO_REUSEPORT'): + if family in (socket.AF_INET, socket.AF_INET6) and addr[1] == 0: + if reuse_port: + warnings.warn( + '''listen on random port (0) with SO_REUSEPORT is dangerous. + Double check your intent. + Example problem: https://github.com/eventlet/eventlet/issues/411''', + ReuseRandomPortWarning, stacklevel=3) + elif reuse_port is None: + reuse_port = True + if reuse_port and hasattr(socket, 'SO_REUSEPORT'): # NOTE(zhengwei): linux kernel >= 3.9 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.bind(addr) diff --git a/tests/convenience_test.py b/tests/convenience_test.py index 37b6cd4..6f996fe 100644 --- a/tests/convenience_test.py +++ b/tests/convenience_test.py @@ -1,7 +1,8 @@ import os +import warnings import eventlet -from eventlet import debug, event +from eventlet import convenience, debug from eventlet.green import socket from eventlet.support import six import tests @@ -90,7 +91,7 @@ class TestServe(tests.LimitedTestCase): gt.wait() def test_concurrency(self): - evt = event.Event() + evt = eventlet.Event() def waiter(sock, addr): sock.sendall(b'hi') @@ -128,18 +129,41 @@ class TestServe(tests.LimitedTestCase): client.sendall(b"echo") self.assertEqual(b"echo", client.recv(1024)) - def test_socket_reuse(self): + +def test_socket_reuse(): + # pick a free port with bind to 0 - without SO_REUSEPORT + # then close it and try to bind to same port with SO_REUSEPORT + # loop helps in case something else used the chosen port before second bind + addr = None + errors = [] + for _ in range(5): lsock1 = eventlet.listen(('localhost', 0)) - port = lsock1.getsockname()[1] - - if hasattr(socket, 'SO_REUSEPORT'): - lsock2 = eventlet.listen(('localhost', port)) - else: - try: - lsock2 = eventlet.listen(('localhost', port)) - assert lsock2 - lsock2.close() - except socket.error: - pass - + addr = lsock1.getsockname() lsock1.close() + try: + lsock1 = eventlet.listen(addr) + except socket.error as e: + errors.append(e) + continue + break + else: + assert False, errors + + if hasattr(socket, 'SO_REUSEPORT'): + lsock2 = eventlet.listen(addr) + else: + try: + lsock2 = eventlet.listen(addr) + assert lsock2 + lsock2.close() + except socket.error: + pass + + lsock1.close() + + +def test_reuse_random_port_warning(): + with warnings.catch_warnings(record=True) as w: + eventlet.listen(('localhost', 0), reuse_port=True).close() + assert len(w) == 1 + assert issubclass(w[0].category, convenience.ReuseRandomPortWarning)