-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
The problem
When SocketsHttpHandler or other middleware building on top of System.Net.Sockets establishes high (>= 16k) number of TCP connections, new connection attempts will fail with socket error 10055 (WSAENOBUFS) because of port exhaustion.
On Windows 10+, this can be avoided by enabling SO_REUSE_UNICASTPORT (SocketOptionName.ReuseUnicastPort). When enabled, socket.Bind() will not allocate a local port, instead, actual port allocation will be deferred until the ConnectEx call, allowing local port reuse and a much higher scale of outbound connections.
Currently the only way to enable SO_REUSE_UNICASTPORT for HttpClient is to override ConnectCallback as following:
https://gist.github.com/antonfirsov/e86ddc9ac287b7dd63cd85f78ca125f6
We had 1 customer hitting this in production, causing port exhaustion.
Proposal
If no explicit bind has been done before connecting a socket, we are doing a wildcard bind:
runtime/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.Windows.cs
Lines 232 to 261 in a473a9c
| partial void WildcardBindForConnectIfNecessary(AddressFamily addressFamily) | |
| { | |
| if (_rightEndPoint != null) | |
| { | |
| return; | |
| } | |
| // The socket must be bound before using ConnectEx. | |
| CachedSerializedEndPoint csep; | |
| switch (addressFamily) | |
| { | |
| case AddressFamily.InterNetwork: | |
| csep = IsDualMode ? | |
| s_cachedMappedAnyV6EndPoint ??= new CachedSerializedEndPoint(s_IPAddressAnyMapToIPv6) : | |
| s_cachedAnyEndPoint ??= new CachedSerializedEndPoint(IPAddress.Any); | |
| break; | |
| case AddressFamily.InterNetworkV6: | |
| csep = s_cachedAnyV6EndPoint ??= new CachedSerializedEndPoint(IPAddress.IPv6Any); | |
| break; | |
| default: | |
| return; | |
| } | |
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, csep.IPEndPoint); | |
| DoBind(csep.IPEndPoint, csep.SocketAddress); | |
| } |
We should enable SocketOptionName.ReuseUnicastPort before binding (by default). This is a non-breaking change, since it is scoped to the internal auto-binding logic.