Skip to content

Conversation

nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Aug 20, 2025

User description

Continuation of #16016

🔗 Related Issues

Fixes #16214

💥 What does this PR do?

Fall back to IPv4

🔧 Implementation Notes

💡 Additional Considerations

🔄 Types of changes

  • Bug fix (backwards compatible)

PR Type

Bug fix


Description

  • Add IPv4 fallback for port finding in IPv6-disabled environments

  • Implement robust error handling for socket creation and binding

  • Maintain IPv6 dual-mode preference with graceful degradation


Diagram Walkthrough

flowchart LR
  A["FindFreePort()"] --> B["Try IPv6 dual-mode socket"]
  B --> C{IPv6 Success?}
  C -->|Yes| D["Return IPv6 port"]
  C -->|No| E["Fallback to IPv4 socket"]
  E --> F["Return IPv4 port"]
Loading

File Walkthrough

Relevant files
Bug fix
PortUtilities.cs
Add IPv4 fallback to port utilities                                           

dotnet/src/webdriver/Internal/PortUtilities.cs

  • Replace simple IPv6 socket creation with try-catch fallback logic
  • Add IPv4 fallback when IPv6 socket creation or binding fails
  • Implement graceful handling of DualMode property failures
  • Maintain existing functionality while adding robustness
+24/-4   

@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Aug 20, 2025
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

16214 - Partially compliant

Compliant requirements:

  • Ensure FindFreePort works on IPv4-only environments without throwing SocketException.
  • Maintain IPv6 support where available and prefer dual-mode when possible.
  • Gracefully fall back to IPv4 if IPv6 bind/creation is not possible.

Non-compliant requirements:

  • Do not regress behavior on machines with both IPv4 and IPv6.

Requires further human verification:

  • Validate no regressions on dual-stack machines (both IPv4 and IPv6) across supported OSes.
  • Run integration tests in an IPv4-only CI agent to confirm SocketException no longer occurs.
  • Verify ChromeDriverService.CreateDefaultService uses this port correctly in downstream flows.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Exception Coverage

Only SocketException is caught; other failures (e.g., ObjectDisposedException, PlatformNotSupportedException) could escape. Consider broadening catch scope or handling specific additional exceptions to ensure robust fallback.

catch(SocketException)
{
    // If creating/binding the IPv6 socket fails for any reason, fall back to IPv4
    using var ipV4socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    ipV4socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
    return ((IPEndPoint)ipV4socket.LocalEndPoint!).Port;
}
DualMode Handling

Silent catch when setting DualMode may hide misconfigurations. Consider logging or at least commenting rationale with a link to known platform behavior to aid diagnostics.

try
{
    ipV6socket.DualMode = true;
}
catch
{
    // Ignored; we'll still attempt to bind to IPv6 loopback
}

Copy link
Contributor

qodo-merge-pro bot commented Aug 20, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Risk of port race condition

Binding to port 0 and returning it without immediately reserving/listening
introduces a TOCTOU race: another process can claim the port between discovery
and later use. Consider returning an already-listening socket/endpoint or
integrating port allocation with the service start to avoid transient free-port
selection.

Examples:

dotnet/src/webdriver/Internal/PortUtilities.cs [34-61]
    public static int FindFreePort()
    {
        // Prefer IPv6 dual-mode when available, but fall back robustly to IPv4
        try
        {
            using var ipV6socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);

            // Some platforms may not support DualMode or IPv6 at all; ignore failures and let bind decide
            try
            {

 ... (clipped 18 lines)

Solution Walkthrough:

Before:

public static int FindFreePort()
{
    // Binds to an ephemeral port (0)
    using var socket = new Socket(...);
    socket.Bind(new IPEndPoint(..., 0));
    int port = ((IPEndPoint)socket.LocalEndPoint!).Port;
    
    // Socket is disposed here, releasing the port
    return port;
}

After:

// Suggestion implies changing the method's contract
public static Socket ReserveFreePort()
{
    var socket = new Socket(...);
    socket.Bind(new IPEndPoint(..., 0));
    // The socket is returned to the caller, keeping the port reserved.
    // The caller is now responsible for closing it.
    return socket;
}

// Example usage by the caller:
var listeningSocket = PortUtilities.ReserveFreePort();
var port = ((IPEndPoint)listeningSocket.LocalEndPoint!).Port;
// ... start service with the reserved socket/port ...
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a pre-existing TOCTOU race condition in the FindFreePort method, which could lead to intermittent failures. While not introduced by this PR, it's a significant design flaw in the code being modified.

Medium
General
Handle DualMode unsupported cases

Setting DualMode can throw NotSupportedException on certain platforms; catching
only SocketException misses that. Catch both to ensure robust fallback without
crashing.

dotnet/src/webdriver/Internal/PortUtilities.cs [39-49]

 using var ipV6socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
 
 // Some platforms may not support DualMode or IPv6 at all; ignore failures and let bind decide
 try
 {
     ipV6socket.DualMode = true;
 }
-catch
-{
-    // Ignored; we'll still attempt to bind to IPv6 loopback
-}
+catch (SocketException) { }
+catch (NotSupportedException) { }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly points out that catching all exceptions is bad practice and accurately identifies that setting DualMode can throw NotSupportedException in addition to SocketException, making the error handling more precise and robust.

Medium
Possible issue
Avoid overbroad exception catching

The inner catch block swallowing all exceptions can hide non-socket failures
(e.g., ObjectDisposedException). Narrow the inner catch to SocketException and
avoid masking unrelated errors. Also, reuse the outer fallback for any bind
failure by rethrowing non-socket exceptions.

dotnet/src/webdriver/Internal/PortUtilities.cs [37-60]

 try
 {
     using var ipV6socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
 
     // Some platforms may not support DualMode or IPv6 at all; ignore failures and let bind decide
     try
     {
         ipV6socket.DualMode = true;
     }
-    catch
+    catch (SocketException)
     {
         // Ignored; we'll still attempt to bind to IPv6 loopback
     }
 
     ipV6socket.Bind(new IPEndPoint(IPAddress.IPv6Loopback, 0));
     return ((IPEndPoint)ipV6socket.LocalEndPoint!).Port;
 }
-catch(SocketException)
+catch (SocketException)
 {
-    // If creating/binding the IPv6 socket fails for any reason, fall back to IPv4
     using var ipV4socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
     ipV4socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
     return ((IPEndPoint)ipV4socket.LocalEndPoint!).Port;
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that the inner catch block is too broad and could hide unrelated errors, proposing a more specific catch (SocketException), which improves code robustness.

Low
  • Update

@nvborisenko
Copy link
Member Author

Interesting input:

IPv4 is still the only truly universal transport today. IPv6 has strong and growing adoption (roughly 40–50% of consumer traffic globally, higher in some countries), but you cannot assume IPv6 availability everywhere, while you still can safely assume IPv4. So if you want to avoid an exception-based probe, default to IPv4 (or a dual‑stack approach when available) and only touch pure IPv6 paths when explicitly needed.

Seems it is better to probe v4 first.

@cgoldberg
Copy link
Contributor

Seems it is better to probe v4 first.

That's what the Python bindings do.

@nvborisenko
Copy link
Member Author

nvborisenko commented Aug 26, 2025

Ready for review. Now we prefer IPv4, and fallback to IPv6.

@nvborisenko
Copy link
Member Author

Thanks, simple and straightforward.

@nvborisenko nvborisenko merged commit 934d13a into SeleniumHQ:trunk Aug 26, 2025
10 checks passed
@nvborisenko nvborisenko deleted the dotnet-ipv4 branch August 26, 2025 20:43
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]: [Dotnet] PortUtilities.FindFreePort SocketException for IPv4 Only
3 participants