diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 60a189bdfb7ec9..c6054623753d67 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -929,29 +929,26 @@ async def create_connection( flags=flags, loop=self) if not laddr_infos: raise OSError('getaddrinfo() returned empty list') + else: + laddr_infos = [(None, None, None, None, None)] exceptions = [] - for family, type, proto, cname, address in infos: + for addrinfo, laddrinfo in itertools.product(infos, laddr_infos): + family, type, proto, cname, address = addrinfo + laddr = laddrinfo[4] try: sock = socket.socket(family=family, type=type, proto=proto) sock.setblocking(False) - if local_addr is not None: - for _, _, _, _, laddr in laddr_infos: - try: - sock.bind(laddr) - break - except OSError as exc: - msg = ( - f'error while attempting to bind on ' - f'address {laddr!r}: ' - f'{exc.strerror.lower()}' - ) - exc = OSError(exc.errno, msg) - exceptions.append(exc) - else: - sock.close() - sock = None - continue + if laddr is not None: + try: + sock.bind(laddr) + except OSError as exc: + msg = ( + f'error while attempting to bind on ' + f'address {laddr!r}: ' + f'{exc.strerror.lower()}' + ) + raise OSError(exc.errno, msg) if self._debug: logger.debug("connect %r to %r", sock, address) await self.sock_connect(sock, address) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 53854758a27d4c..b3f359da402d05 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1367,6 +1367,44 @@ def getaddrinfo_task(*args, **kwds): self.assertRaises( OSError, self.loop.run_until_complete, coro) + @patch_socket + def test_create_connection_socket_bind_different_family(self, m_socket): + # https://bugs.python.org/issue35302 + # Test the case where local_addr resolves to multiple addrinfos, and + # if the socket is bound to the first address, connect() will fail. + sock = m_socket.socket.return_value + + self.loop._add_reader = mock.Mock() + self.loop._add_reader._is_coroutine = False + self.loop._add_writer = mock.Mock() + self.loop._add_writer._is_coroutine = False + + def mock_connect_check_bind(address): + if sock.bind.call_count: + bind_addr = sock.bind.call_args[0][0] + if bind_addr[0][:4] != address[0][:4]: + raise OSError + + sock.connect.side_effect = mock_connect_check_bind + + async def getaddrinfo(host, port, *args, **kwargs): + if host == 'bindaddr': + return [(2, 1, 6, '', ('192.0.2.1', port)), + (10, 1, 6, '', ('2001:db8::1', port))] + else: + return [(10, 1, 6, '', ('2001:db8::1:1', port))] + + self.loop.getaddrinfo = getaddrinfo + + coro = self.loop.create_connection( + MyProto, 'connectaddr', 80, local_addr=('bindaddr', 0)) + t, p = self.loop.run_until_complete(coro) + try: + sock.connect.assert_called_with(('2001:db8::1:1', 80)) + finally: + t.close() + test_utils.run_briefly(self.loop) + @patch_socket def test_create_connection_bluetooth(self, m_socket): # See http://bugs.python.org/issue27136, fallback to getaddrinfo when diff --git a/Misc/NEWS.d/next/Library/2019-03-31-14-50-10.bpo-35302.IDJZzC.rst b/Misc/NEWS.d/next/Library/2019-03-31-14-50-10.bpo-35302.IDJZzC.rst new file mode 100644 index 00000000000000..bb6ddbcffc7102 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-03-31-14-50-10.bpo-35302.IDJZzC.rst @@ -0,0 +1,2 @@ +Try each (remote addrinfo, local addrinfo) pair in +BaseEventLoop.create_connection().