dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.47k stars 4.76k forks source link

Socket on Unix behaves inconsistently between IPAddress[] and DnsEndPoint connects #110124

Open stephentoub opened 1 week ago

stephentoub commented 1 week ago

A socket on unix doesn't support reconnecting with the same file descriptor after a failed connection. This has created an issue for the .NET Socket design, whereby options can be set onto a Socket instance, and then that Socket instance can be connected to something that might actually require multiple attempts. To make this work, the Socket tracks known settings, and recreates the file descriptor under the covers upon a failed attempt. If an unknown setting is used, Socket refuses to connect to an endpoint that might possibly require multiple attempts, e.g. a DnsEndPoint. Unfortunately, we're not applying this consistently across such connect methods.

This:

using System.Net;
using System.Net.Sockets;

Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 1);
await s.ConnectAsync(new DnsEndPoint("microsoft.com", 80));

throws the exception:

Unhandled exception. System.PlatformNotSupportedException: Sockets on this platform are invalid for use after a failed connection attempt.
   at System.Net.Sockets.Socket.ThrowMultiConnectNotSupported()
   at System.Net.Sockets.Socket.ConnectAsync(SocketAsyncEventArgs e, Boolean userSocket, Boolean saeaCancelable)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ConnectAsync(Socket socket)
   at System.Net.Sockets.Socket.ConnectAsync(EndPoint remoteEP, CancellationToken cancellationToken)
   at System.Net.Sockets.Socket.ConnectAsync(EndPoint remoteEP)
   at Program.<Main>$(String[] args)

But this:

using System.Net;
using System.Net.Sockets;

Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 1);
await s.ConnectAsync(Dns.GetHostAddresses("microsoft.com"), 80);

completes successfully, even though they're logically the same, with the former just doing the DNS lookup and effectively then behaving as if that list was provided.

At this point, it'd likely be a significant breaking change to make the latter fail like the former.

We should reconsider the behavior for the former, and possibly revisit the whole retry mechanism, caching more aggressively such that few-to-no settings knock us off the golden path.

dotnet-policy-service[bot] commented 1 week ago

Tagging subscribers to this area: @dotnet/ncl See info in area-owners.md if you want to be subscribed.