Skip to content

[py] Allow free_port() to bind to IPv6 if IPv4 is unavailable #16003

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

Open
wants to merge 21 commits into
base: trunk
Choose a base branch
from

Conversation

cgoldberg
Copy link
Contributor

@cgoldberg cgoldberg commented Jul 6, 2025

User description

🔗 Related Issues

Fixes #14910 for Python

💥 What does this PR do?

This PR updates the free_port() function in selenium.webdriver.common.utils so it will bind to localhost using IPv6 (::1) if IPv4 (127.0.0.1) is not available.

Without this fix, Selenium can't be used on an IPv6-only system without some additional code.

For example, if you are on IPv6-only system, the following code will fail:

from selenium import webdriver
driver = webdriver.Chrome()

with OSError: [Errno 99] Cannot assign requested address

With the changes in the PR, it will work.

Note: geckodriver doesn't work on IPv6-only systems, so Firefox still won't work.


Notes on testing:

I didn't have an IPv6-only system to test on, so I used a a mixed environment and disabled IPv4 interfaces. Here are instructions for setting that up on Debian/Ubuntu Linux:

To see interface names and IP addresses, run ip addr show.

For example, I get the following output:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:16:3e:70:a6:12 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 100.115.92.196/28 brd 100.115.92.207 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2601:189:8181:33b0:216:3eff:fe70:a612/64 scope global dynamic mngtmpaddr 
       valid_lft 345599sec preferred_lft 345599sec
    inet6 fe80::216:3eff:fe70:a612/64 scope link 
       valid_lft forever preferred_lft forever

You can see I have the following network interfaces with IPv4 addresses:

lo: 127.0.0.1/8
eth0: 100.115.92.196/28

I can disable these by running:

sudo ip addr del 100.115.92.196/28 dev eth0
sudo ip addr del 127.0.0.1/8 dev lo

Now I am running on a system that only allows IPv6 and can test.

(these changes will be reverted on reboot)

🔄 Types of changes

  • Bug fix (backwards compatible)

PR Type

Enhancement


Description

  • Add IPv6 fallback support to free_port() function

  • Enable Selenium to work on IPv6-only systems

  • Maintain backward compatibility with IPv4 systems


Changes diagram

flowchart LR
  A["free_port() called"] --> B["Try IPv4 bind"]
  B --> C{IPv4 available?}
  C -->|Yes| D["Bind to 127.0.0.1"]
  C -->|No| E["Fallback to IPv6"]
  E --> F["Bind to ::1"]
  D --> G["Return port"]
  F --> G
Loading

Changes walkthrough 📝

Relevant files
Enhancement
utils.py
Add IPv6 fallback to free_port function                                   

py/selenium/webdriver/common/utils.py

  • Modified free_port() to try IPv4 first, fallback to IPv6
  • Added exception handling for OSError when IPv4 binding fails
  • Updated function docstring to explain IPv6 fallback behavior
  • +12/-3   

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • @selenium-ci selenium-ci added the C-py Python Bindings label Jul 6, 2025
    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 6, 2025

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified

    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 6, 2025

    PR Code Suggestions ✨

    Latest suggestions up to e1b3716

    CategorySuggestion                                                                                                                                    Impact
    Incremental [*]
    Handle IPv6 binding failures gracefully
    Suggestion Impact:The suggestion was implemented by wrapping the IPv6 socket creation and binding in a try-except block, with a RuntimeError raised when both IPv4 and IPv6 fail

    code diff:

    +        try:
    +            free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    +            free_socket.bind(("::1", 0))
    +        except OSError:
    +            raise RuntimeError("Can't find free port (Unable to bind to IPv4 or IPv6)")

    Add a try-except block around the IPv6 socket creation and binding to handle
    cases where IPv6 is also unavailable, preventing potential unhandled exceptions
    that could crash the function.

    py/selenium/webdriver/common/utils.py [34-44]

     free_socket = None
     try:
         # IPv4
         free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         free_socket.bind(("127.0.0.1", 0))
     except OSError:
         if free_socket:
             free_socket.close()
    -    # IPv6
    -    free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    -    free_socket.bind(("::1", 0))
    +    try:
    +        # IPv6
    +        free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    +        free_socket.bind(("::1", 0))
    +    except OSError:
    +        raise RuntimeError("Unable to bind to both IPv4 and IPv6")

    [Suggestion processed]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion correctly identifies that if both IPv4 and IPv6 binding fail, an unhandled OSError would occur, and proposes a robust solution to catch this.

    Medium
    Learned
    best practice
    Ensure proper socket cleanup
    Suggestion Impact:The suggestion was implemented with the try-finally block to ensure socket cleanup, but the commit also added additional error handling and exception wrapping that wasn't in the original suggestion

    code diff:

    +    try:
    +        free_socket.listen(5)
    +        port: int = free_socket.getsockname()[1]
    +    except Exception as e:
    +        raise RuntimeError(f"Can't find free port ({e})")
    +    finally:
    +        free_socket.close()

    The socket resource is not properly cleaned up if an exception occurs after the
    IPv6 socket creation or during listen/getsockname operations. Use a try-finally
    block to ensure the socket is always closed, preventing resource leaks.

    py/selenium/webdriver/common/utils.py [45-47]

     free_socket = None
     try:
         # IPv4
         free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         free_socket.bind(("127.0.0.1", 0))
     except OSError:
         if free_socket:
             free_socket.close()
         # IPv6
         free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
         free_socket.bind(("::1", 0))
    -free_socket.listen(5)
    -port: int = free_socket.getsockname()[1]
    -free_socket.close()
     
    +try:
    +    free_socket.listen(5)
    +    port: int = free_socket.getsockname()[1]
    +finally:
    +    free_socket.close()
    +

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 6

    __

    Why:
    Relevant best practice - Ensure proper resource cleanup by closing sockets, processes, and drivers in finally blocks or using context managers, and check process state before attempting to terminate to prevent resource leaks and exceptions.

    Low
    • Update

    Previous suggestions

    ✅ Suggestions up to commit ee82768
    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Prevent socket resource leak
    Suggestion Impact:The suggestion was directly implemented - the commit adds the exact code changes suggested to prevent socket resource leaks by initializing free_socket to None and closing it if binding fails

    code diff:

    +    free_socket = None
         try:
             # IPv4
             free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             free_socket.bind(("127.0.0.1", 0))
         except OSError:
    +        if free_socket:
    +            free_socket.close()

    The IPv4 socket should be closed if binding fails to prevent resource leaks.
    Currently, if IPv4 socket creation succeeds but binding fails, the socket
    remains open when the exception is caught.

    py/selenium/webdriver/common/utils.py [34-44]

    +free_socket = None
     try:
         # IPv4
         free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         free_socket.bind(("127.0.0.1", 0))
     except OSError:
    +    if free_socket:
    +        free_socket.close()
         # IPv6
         free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
         free_socket.bind(("::1", 0))
     free_socket.listen(5)
     port: int = free_socket.getsockname()[1]
     free_socket.close()

    [Suggestion processed]

    Suggestion importance[1-10]: 8

    __

    Why: The suggestion correctly identifies a resource leak where the IPv4 socket is not closed if bind() fails, which is a valid correctness issue introduced in the PR.

    Medium

    @nvborisenko
    Copy link
    Member

    @cgoldberg do you know how to easily test it?

    @cgoldberg
    Copy link
    Contributor Author

    @nvborisenko read my PR ... I wrote a whole section on how to test it :)

    (for Linux at least)

    @nvborisenko
    Copy link
    Member

    I tried your steps to disable IPv4 interfaces on Ubuntu VM, and even cannot reproduce the issue when finding available port should fail :(

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    [🐛 Bug]: FindFreePort method hardcoded for IPv4 causing failure for pure IPv6 environments
    4 participants