Skip to content

Commit

Permalink
🐛 fix fast reuse outgoing port resulting in exception as we did not a…
Browse files Browse the repository at this point in the history
…ssign SO_REUSEPORT or SO_REUSEADDR prior (#169)
  • Loading branch information
Ousret authored Oct 26, 2024
1 parent d8f21eb commit 97fa608
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
2.11.906 (2024-10-26)
=====================

- Fixed unexpected exception when recreating a connection using the same outgoing port.
Add ``SO_REUSEPORT`` if available, fallback to ``SO_REUSEADDR``. This socket option
is not bullet proof against reusability errors. Some OS differs in behaviors.

2.11.905 (2024-10-26)
=====================

Expand Down
2 changes: 1 addition & 1 deletion src/urllib3/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This file is protected via CODEOWNERS
from __future__ import annotations

__version__ = "2.11.905"
__version__ = "2.11.906"
5 changes: 5 additions & 0 deletions src/urllib3/backend/hface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,11 @@ def close(self) -> None:
): # don't want our goodbye, never mind then!
break

try:
self.sock.shutdown(0)
except (OSError, AttributeError):
pass

try:
self.sock.close()
except OSError:
Expand Down
14 changes: 14 additions & 0 deletions src/urllib3/contrib/resolver/_async/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ async def create_connection( # type: ignore[override]
try:
sock = AsyncSocket(af, socktype, proto)

# we need to add this or reusing the same origin port will likely fail within
# short period of time. kernel put port on wait shut.
if source_address:
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except (OSError, AttributeError): # Defensive: very old OS?
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except (
OSError,
AttributeError,
): # Defensive: we can't do anything better than this.
pass

# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)

Expand Down
14 changes: 14 additions & 0 deletions src/urllib3/contrib/resolver/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ def create_connection(
try:
sock = socket.socket(af, socktype, proto)

# we need to add this or reusing the same origin port will likely fail within
# short period of time. kernel put port on wait shut.
if source_address is not None:
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except (OSError, AttributeError): # Defensive: very old OS?
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except (
OSError,
AttributeError,
): # Defensive: we can't do anything better than this.
pass

# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)

Expand Down
20 changes: 20 additions & 0 deletions test/with_traefik/asynchronous/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,23 @@ async def test_quic_extract_ssl_ctx_ca_root(self) -> None:

assert resp.status == 200
assert resp.version == 30

@pytest.mark.xfail(
reason="experimental support for reusable outgoing port", strict=False
)
async def test_fast_reuse_outgoing_port(self) -> None:
for _ in range(4):
conn = AsyncHTTPSConnection(
self.host,
self.https_port,
ca_certs=self.ca_authority,
resolver=self.test_async_resolver.new(),
source_address=("0.0.0.0", 8745),
)

await conn.request("GET", "/get")
resp = await conn.getresponse()

assert resp.status == 200

await conn.close()
20 changes: 20 additions & 0 deletions test/with_traefik/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,23 @@ def test_quic_extract_ssl_ctx_ca_root(self) -> None:

assert resp.status == 200
assert resp.version == 30

@pytest.mark.xfail(
reason="experimental support for reusable outgoing port", strict=False
)
def test_fast_reuse_outgoing_port(self) -> None:
for _ in range(4):
conn = HTTPSConnection(
self.host,
self.https_port,
ca_certs=self.ca_authority,
resolver=self.test_resolver.new(),
source_address=("0.0.0.0", 8845),
)

conn.request("GET", "/get")
resp = conn.getresponse()

assert resp.status == 200

conn.close()

0 comments on commit 97fa608

Please sign in to comment.