Skip to content

Flaky SO_REUSEPORT/SO_REUSEADDR client socket #126539

@Ousret

Description

@Ousret

Bug report

Bug description:

Unable to reliably be able to reuse a outgoing port using a client socket.
Depending on the interpreter version and/or OS, it sometime fails, sometime succeed.

import niquests

if __name__ == "__main__":

    with niquests.Session(source_address=("0.0.0.0", 8755)) as s:
        print(s.get("https://1.1.1.1"))

    with niquests.Session(source_address=("0.0.0.0", 8755)) as s:
        print(s.get("https://1.1.1.1"))
import niquests
import asyncio

async def main():
    async with niquests.AsyncSession(source_address=("0.0.0.0", 8755)) as s:
        print(await s.get("https://1.1.1.1"))

    async with niquests.AsyncSession(source_address=("0.0.0.0", 8755)) as s:
        print(await s.get("https://1.1.1.1"))

if __name__ == "__main__":
    asyncio.run(main())

You should get intermittent [Errno 99] Cannot assign requested address or similar depending on the OS.

I ran the following in:

  • Windows
  • MacOS
  • Linux

Across Python 3.7 -- 3.13

Here are the results:

  • Linux 3.7 (sync OK, async OK)

  • Windows 3.7 (sync OK, async OK)

  • Linux 3.11 (sync OK, async KO)

  • Windows 3.11 (sync OK, async KO)

  • Windows 3.10 (sync KO, async KO)

  • MacOS 3.8+ (sync KO, async KO)

Curiously, if you ran:

import niquests

if __name__ == "__main__":

    with niquests.Session(source_address=("0.0.0.0", 8755)) as s:
        print(s.get("https://1.1.1.1"))

By running the interpreter twice (exec python sample.py twice or more), it will work as much as needed. Something happen at interpreter shutdown that should happen before?

Low level speaking, socket.SO_REUSEPORT is applied when available, otherwise using socket.SO_REUSEADDR instead.
The sock.bind((addr, port)) is applied after setting sock opts and before connecting to remote peer.

As it seems to work flawlessly on Python 3.7, I expected it to work on later versions also.

See the minimal code to reproduce this (sync only):

import socket

def cpython_bug_bind_so_reuseport():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    except (AttributeError, OSError):  # Windows branch or old OS
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    sock.bind(("0.0.0.0", 5784))
    sock.connect(("1.1.1.1", 443))

    sock.shutdown(socket.SHUT_RD)
    sock.close()


if __name__ == "__main__":
    cpython_bug_bind_so_reuseport()
    cpython_bug_bind_so_reuseport()

I posted the "higher" level code because, sometime the execution does not raise "Cannot assign requested address" but timeout instead. So the bind and connect pass but the socket is unusable. You will have to insist a bit to get this behavior.

Did I miss something? The official docs does not clearly mention .bind(..) usage with client-side socket, so we're in a grey area.

Regards,

CPython versions tested on:

3.9, 3.10, 3.11, 3.12, 3.13

Operating systems tested on:

Linux, macOS, Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions