Skip to content

[dotnet] Support IPv6 to find free tcp port via DualMode #16016

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 10 commits into
base: trunk
Choose a base branch
from

Conversation

nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Jul 7, 2025

User description

💥 What does this PR do?

Contributes to #14910

🔧 Implementation Notes

Seems it might throw here (if dual mode not supported?):

socket.DualMode = true

Then in future just wrap it to try/catch and fallback to ipv4 only.


PR Type

Enhancement


Description

  • Replace Socket-based port finding with TcpListener approach

  • Add IPv6 support with IPv4 fallback mechanism

  • Improve resource disposal and error handling

  • Simplify code structure and remove manual socket management


Changes diagram

flowchart LR
  A["Old Socket approach"] --> B["New TcpListener approach"]
  B --> C["Try IPv6Any with DualMode"]
  C --> D["Success: Return port"]
  C --> E["SocketException: Fallback to IPv4"]
  E --> F["Return IPv4 port"]
Loading

Changes walkthrough 📝

Relevant files
Enhancement
PortUtilities.cs
Modernize port finding with IPv6 support                                 

dotnet/src/webdriver/Internal/PortUtilities.cs

  • Replace Socket with TcpListener for port finding
  • Add IPv6 support with DualMode enabled
  • Implement IPv4 fallback on SocketException
  • Simplify resource management with automatic disposal
  • +13/-13 

    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-dotnet .NET Bindings label Jul 7, 2025
    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 7, 2025

    PR Reviewer Guide 🔍

    (Review updated until commit c37403d)

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Resource Leak

    TcpListener instances are not properly disposed. If an exception occurs between Start() and Stop(), the listener may remain open, potentially causing resource leaks.

        var listener = new TcpListener(IPAddress.IPv6Any, 0);
        listener.Server.DualMode = true; // Listen on both IPv4 and IPv6
        listener.Start();
        int port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }
    catch (SocketException)
    {
        // If IPv6Any is not supported, fallback to IPv4
        var listener = new TcpListener(IPAddress.Any, 0);
        listener.Start();
        int port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    Race Condition

    There's a potential race condition between stopping the listener and returning the port. Another process could bind to the same port before the caller uses it.

    int port = ((IPEndPoint)listener.LocalEndpoint).Port;
    listener.Stop();
    return port;

    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 7, 2025

    PR Code Suggestions ✨

    Latest suggestions up to c37403d
    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    General
    Add proper resource disposal handling

    The IPv4 fallback listener is not properly disposed if an exception occurs
    during its operation. Wrap the fallback listener in a using statement or
    try-finally block to ensure proper resource cleanup.

    dotnet/src/webdriver/Internal/PortUtilities.cs [45-53]

     catch (SocketException)
     {
         // If IPv6Any is not supported, fallback to IPv4
         var listener = new TcpListener(IPAddress.Any, 0);
    -    listener.Start();
    -    int port = ((IPEndPoint)listener.LocalEndpoint).Port;
    -    listener.Stop();
    -    return port;
    +    try
    +    {
    +        listener.Start();
    +        int port = ((IPEndPoint)listener.LocalEndpoint).Port;
    +        return port;
    +    }
    +    finally
    +    {
    +        listener.Stop();
    +    }
     }
    • Apply / Chat
    Suggestion importance[1-10]: 7

    __

    Why: The suggestion correctly identifies a potential resource leak in the catch block, as an exception after new TcpListener would prevent listener.Stop() from being called.

    Medium
    Learned
    best practice
    Use proper resource disposal patterns

    The TcpListener resources are not properly disposed in case of exceptions. Use
    using statements or try-finally blocks to ensure proper cleanup. This prevents
    resource leaks if exceptions occur between Start() and Stop() calls.

    dotnet/src/webdriver/Internal/PortUtilities.cs [38-43]

    -var listener = new TcpListener(IPAddress.IPv6Any, 0);
    +using var listener = new TcpListener(IPAddress.IPv6Any, 0);
     listener.Server.DualMode = true; // Listen on both IPv4 and IPv6
     listener.Start();
     int port = ((IPEndPoint)listener.LocalEndpoint).Port;
    -listener.Stop();
     return port;
    • Apply / Chat
    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 7271623
    CategorySuggestion                                                                                                                                    Impact
    General
    Preserve original socket exception details
    Suggestion Impact:The commit completely refactored the port finding approach from using raw Socket to TcpListener, which eliminated the SocketException handling entirely. The exception handling code was removed as part of this refactoring.

    code diff:

    -        catch (SocketException ex)
    +        finally
             {
    -            throw new InvalidOperationException("Unable to find a free port.", ex);
    +            tcpListener.Stop();
             }

    The exception handling is too broad and may mask legitimate socket binding
    failures. Consider allowing the method to retry with different approaches or
    return a more specific error that indicates the system state rather than
    throwing immediately.

    dotnet/src/webdriver/Internal/PortUtilities.cs [45-48]

     catch (SocketException ex)
     {
    -    throw new InvalidOperationException("Unable to find a free port.", ex);
    +    // Let the original SocketException bubble up as it provides more specific information
    +    // about why port binding failed (e.g., no available ports, permission issues)
    +    throw;
     }

    [Suggestion processed]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion correctly identifies that wrapping SocketException in InvalidOperationException obscures the original, more specific error, making debugging harder.

    Medium
    Learned
    best practice
    Add null validation for LocalEndPoint

    The code uses null-forgiving operator (!) on LocalEndPoint without proper null
    checking, which could lead to NullReferenceException if LocalEndPoint is null.
    Add explicit null validation before accessing the Port property to ensure safe
    execution.

    dotnet/src/webdriver/Internal/PortUtilities.cs [43]

    -return ((IPEndPoint)socket.LocalEndPoint!).Port;
    +var localEndPoint = socket.LocalEndPoint as IPEndPoint;
    +if (localEndPoint == null)
    +    throw new InvalidOperationException("Failed to get local endpoint from socket.");
    +return localEndPoint.Port;
    Suggestion importance[1-10]: 6

    __

    Why:
    Relevant best practice - Add null checks and validation for parameters, properties, and return values before using them to prevent NullReferenceException and other runtime errors

    Low

    @nvborisenko nvborisenko requested a review from Copilot July 7, 2025 20:15
    Copilot

    This comment was marked as outdated.

    @nvborisenko nvborisenko marked this pull request as draft July 8, 2025 20:40
    @nvborisenko nvborisenko changed the title [dotnet] Dispose socket object to find free tcp port [dotnet] Use primary IPv6 tcp listener to find free tcp port and fallback to IPv4 Jul 8, 2025
    @nvborisenko nvborisenko marked this pull request as ready for review July 8, 2025 20:56
    @nvborisenko
    Copy link
    Member Author

    I am done, but I don't know how to test it.

    @cgoldberg
    Copy link
    Contributor

    I don't know how to test it.

    On Linux, you can do something similar to what I describe in #16003. You probably want to test on a system setup for IPv4-only, IPv6-only, and Mixed/Dual-stack. I'm not sure how to approach that on other Operating Systems.

    @nvborisenko
    Copy link
    Member Author

    This implementation via TcpListener is even faster than before, but we should to test it somehow. I still cannot figure out how to test it in different environments.

    Plan B is test on production, and revert these changes as soon as users start posting new issues.

    @cgoldberg
    Copy link
    Contributor

    Would it make sense to change the order (try IPv4 first)? That's what I did in Python.

    It doesn't make much difference, but I think it's more likely that a system is IPv4-only than IPv6-only (with dual-stack interface, it wouldn't matter)... so there wouldn't be the overhead of an exception in the more common scenario.

    @nvborisenko
    Copy link
    Member Author

    I got IPv6 only test environment where I am able to test old/new behavior. Give me a chance to fine tune this PR.

    @nvborisenko nvborisenko changed the title [dotnet] Use primary IPv6 tcp listener to find free tcp port and fallback to IPv4 [dotnet] Use primary IPv6 to find free tcp port and fallback to IPv4 Jul 11, 2025
    @nvborisenko nvborisenko requested a review from Copilot July 11, 2025 16:42
    Copy link

    @Copilot Copilot AI left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Pull Request Overview

    This PR refactors the free port discovery method to prefer an IPv6 socket binding (with automatic disposal) and fallback to IPv4 on SocketException.

    • Attempt binding an IPv6 socket to a loopback endpoint and return its port
    • Catch SocketException to retry binding on an IPv4 loopback endpoint
    • Simplify resource management with using var instead of manual socket lifecycle
    Comments suppressed due to low confidence (1)

    dotnet/src/webdriver/Internal/PortUtilities.cs:41

    • There’s no test covering the IPv6 binding failure path; consider adding a unit test that simulates or forces a SocketException on IPv6 to verify the IPv4 fallback.
                using var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
    

    @nvborisenko
    Copy link
    Member Author

    IMHO it is done, there is no try/catch performance penalty. I tested it on "v4 + v6" and "v6 only". Hopefully it works on "v4 only".

    Now plan B is as soon as somebody reports that dual mode is not supported, then we will try/catch his environment and fall back to v4.

    @nvborisenko nvborisenko changed the title [dotnet] Use primary IPv6 to find free tcp port and fallback to IPv4 [dotnet] Support IPv6 to find free tcp port via DualMode Jul 12, 2025
    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.

    3 participants