From 6ac27d9336ef681b84765d1aae9f64f0a08a7955 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Tue, 18 May 2021 20:26:13 +0100 Subject: [PATCH 1/9] Add support for configuring Kestrel listen and accept sockets As mentioned in #32794 it is currently not possible to configure the underlying listen and accept sockets used by Kestrel. In certain circumstances it is desirable to be able to configure socket options - a concrete case that I recently came across is setting the `SO_RECV_ANYIF` socket option on macOS so that Kestrel can listen on the `awdl0` interface. This change adds the API suggested by #32794 with some tweaks, notably splitting the configuration of the _listen_ socket from the _accept_ socket. On some platforms (*nix at least, not sure about Windows) the accept socket does not appear to inherit the socket options configured on the listen socket. So, I've added: - `Action? ConfigureListenSocket { get; set; }` which allows the listen socket to be configured. - `Action? ConfigureAcceptSocket { get; set; }` which allows accept sockets to be configured. There's also some tests using IPv4, IPv6 and unix domain sockets. I have no idea how to use other kinds of `EndPoint` (e.g. `FileHandleEndPoint`) with Kestrel so have left those out of the tests. Happy to add them to get the additional coverage - just need some pointers on how to use. --- .../src/PublicAPI.Unshipped.txt | 4 + .../src/SocketConnectionListener.cs | 9 ++ .../src/SocketTransportOptions.cs | 12 ++ .../SocketTransportOptionsTests.cs | 114 ++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index 7daf5c9863fe..a5d7de6d9301 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -7,3 +7,7 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.Bin ~Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptSocket.get -> System.Action? +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptSocket.set -> void +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureListenSocket.get -> System.Action? +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureListenSocket.set -> void \ No newline at end of file diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index 6b618e5d5f76..8bd9d3f7af8a 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -98,9 +98,11 @@ internal void Bind() case FileHandleEndPoint fileHandle: _socketHandle = new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true); listenSocket = new Socket(_socketHandle); + ConfigureSocket(); break; case UnixDomainSocketEndPoint unix: listenSocket = new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified); + ConfigureSocket(); BindSocket(); break; case IPEndPoint ip: @@ -111,14 +113,19 @@ internal void Bind() { listenSocket.DualMode = true; } + + ConfigureSocket(); BindSocket(); break; default: listenSocket = new Socket(EndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + ConfigureSocket(); BindSocket(); break; } + void ConfigureSocket() => _options.ConfigureListenSocket?.Invoke(EndPoint, listenSocket); + void BindSocket() { try @@ -155,6 +162,8 @@ void BindSocket() acceptSocket.NoDelay = _options.NoDelay; } + _options.ConfigureAcceptSocket?.Invoke(EndPoint, acceptSocket); + var setting = _settings[_settingsIndex]; var connection = new SocketConnection(acceptSocket, diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 1c6ab6114886..57960bc07ece 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -3,6 +3,8 @@ using System; using System.Buffers; +using System.Net; +using System.Net.Sockets; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets { @@ -65,6 +67,16 @@ public class SocketTransportOptions /// public bool UnsafePreferInlineScheduling { get; set; } + /// + /// An action used to configure the listening socket before it is bound. + /// + public Action? ConfigureListenSocket { get; set; } + + /// + /// An action used to configure an accept socket before it's passed to the underlying connection. + /// + public Action? ConfigureAcceptSocket { get; set; } + internal Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; } } diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs new file mode 100644 index 000000000000..6d0b614e82c3 --- /dev/null +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class SocketTransportOptionsTests : LoggedTestBase + { + [Theory] + [MemberData(nameof(GetEndpoints))] + public async Task SocketTransportCallsConfigureListenSocket(EndPoint endpointToTest) + { + var wasCalled = false; + void ConfigureListenSocket(EndPoint endpoint, Socket socket) => wasCalled = true; + + using var host = CreateWebHost( + endpointToTest, options => options.ConfigureListenSocket = ConfigureListenSocket + ); + await host.StartAsync(); + Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.ConfigureListenSocket)} to be called."); + await host.StopAsync(); + } + + [Theory] + [MemberData(nameof(GetEndpoints))] + public async Task SocketTransportCallsConfigureAcceptSocket(EndPoint endpointToTest) + { + var wasCalled = false; + void ConfigureAcceptSocket(EndPoint endpoint, Socket socket) => wasCalled = true; + + using var host = CreateWebHost( + endpointToTest, options => options.ConfigureAcceptSocket = ConfigureAcceptSocket + ); + using var client = CreateHttpClient(endpointToTest); + await host.StartAsync(); + var uri = host.GetUris().First(); + var response = await client.GetAsync(uri); + response.EnsureSuccessStatusCode(); + Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.ConfigureAcceptSocket)} to be called."); + await host.StopAsync(); + } + + private static int _counter = 1; + public static IEnumerable GetEndpoints() + { + // IPv4 + yield return new object[] {new IPEndPoint(IPAddress.Loopback, 0)}; + // IPv6 + yield return new object[] {new IPEndPoint(IPAddress.IPv6Loopback, 0)}; + // Unix sockets + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + yield return new object[] + { + // NOTE + // to avoid "address in use" errors need to ensure + // the socket path is unique + new UnixDomainSocketEndPoint( + $"/tmp/test-{Interlocked.Increment(ref _counter)}.sock" + ) + }; + } + + // TODO: other endpoint types? + } + + private IHost CreateWebHost(EndPoint endpoint, Action configureSocketOptions) => + TransportSelector.GetHostBuilder() + .ConfigureWebHost( + webHostBuilder => + { + webHostBuilder + .UseSockets(configureSocketOptions) + .UseKestrel(options => options.Listen(endpoint)) + .Configure( + app => app.Run(ctx => ctx.Response.WriteAsync("Hello World")) + ); + } + ) + .ConfigureServices(AddTestLogging) + .Build(); + private static HttpClient CreateHttpClient(EndPoint endpoint) + { + if (endpoint is UnixDomainSocketEndPoint) + { + // https://stackoverflow.com/a/67203488/871146 + return new HttpClient(new SocketsHttpHandler + { + ConnectCallback = async (_, _) => + { + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); + await socket.ConnectAsync(endpoint); + return new NetworkStream(socket, ownsSocket: true); + } + }); + } + + return new HttpClient(); + } + } +} From 47b9a6c15b421d6f956793a9e252f160819b6ff0 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Wed, 19 May 2021 12:24:46 +0100 Subject: [PATCH 2/9] `ConfigureAcceptSocket` => `ConfigureAcceptedSocket` --- .../Transport.Sockets/src/SocketConnectionListener.cs | 2 +- .../Transport.Sockets/src/SocketTransportOptions.cs | 4 ++-- .../test/Sockets.BindTests/SocketTransportOptionsTests.cs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index 8bd9d3f7af8a..d9cf4991389b 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -162,7 +162,7 @@ void BindSocket() acceptSocket.NoDelay = _options.NoDelay; } - _options.ConfigureAcceptSocket?.Invoke(EndPoint, acceptSocket); + _options.ConfigureAcceptedSocket?.Invoke(EndPoint, acceptSocket); var setting = _settings[_settingsIndex]; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 57960bc07ece..f45c737dc947 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -73,9 +73,9 @@ public class SocketTransportOptions public Action? ConfigureListenSocket { get; set; } /// - /// An action used to configure an accept socket before it's passed to the underlying connection. + /// An action used to configure an accepted socket before it's passed to the underlying connection. /// - public Action? ConfigureAcceptSocket { get; set; } + public Action? ConfigureAcceptedSocket { get; set; } internal Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; } diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs index 6d0b614e82c3..8d275963ae7a 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs @@ -36,20 +36,20 @@ public async Task SocketTransportCallsConfigureListenSocket(EndPoint endpointToT [Theory] [MemberData(nameof(GetEndpoints))] - public async Task SocketTransportCallsConfigureAcceptSocket(EndPoint endpointToTest) + public async Task SocketTransportCallsConfigureAcceptedSocket(EndPoint endpointToTest) { var wasCalled = false; - void ConfigureAcceptSocket(EndPoint endpoint, Socket socket) => wasCalled = true; + void ConfigureAcceptedSocket(EndPoint endpoint, Socket socket) => wasCalled = true; using var host = CreateWebHost( - endpointToTest, options => options.ConfigureAcceptSocket = ConfigureAcceptSocket + endpointToTest, options => options.ConfigureAcceptedSocket = ConfigureAcceptedSocket ); using var client = CreateHttpClient(endpointToTest); await host.StartAsync(); var uri = host.GetUris().First(); var response = await client.GetAsync(uri); response.EnsureSuccessStatusCode(); - Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.ConfigureAcceptSocket)} to be called."); + Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.ConfigureAcceptedSocket)} to be called."); await host.StopAsync(); } From c1ba647286689c2e758f0217d7dd7a8857851123 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Wed, 19 May 2021 12:28:32 +0100 Subject: [PATCH 3/9] Update public API bits --- .../Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index a5d7de6d9301..2c5391731922 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -7,7 +7,7 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.Bin ~Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptSocket.get -> System.Action? -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptSocket.set -> void +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptedSocket.get -> System.Action? +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptedSocket.set -> void Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureListenSocket.get -> System.Action? Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureListenSocket.set -> void \ No newline at end of file From 52deeeefa5be16b7cad2a945b9a508697398afcc Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Wed, 19 May 2021 21:12:09 +0100 Subject: [PATCH 4/9] Update src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs Co-authored-by: Stephen Halter --- .../Kestrel/Transport.Sockets/src/SocketTransportOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index f45c737dc947..f4f0446e3943 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -73,7 +73,7 @@ public class SocketTransportOptions public Action? ConfigureListenSocket { get; set; } /// - /// An action used to configure an accepted socket before it's passed to the underlying connection. + /// An action used to configure an accepted socket before it's used. /// public Action? ConfigureAcceptedSocket { get; set; } From 7a4110d199bbfb233e87cfb4993b1caef292b124 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Thu, 20 May 2021 18:21:18 +0100 Subject: [PATCH 5/9] Move to using API from triage This removes `ConfigureAcceptedSocket` and changes `ConfigureListenSocket` to be a factory for the socket. A static method that creates the default socket is defined in `SocketTransportOptions.CreateDefaultListenSocket` - this effectively lifts the code that created a socket for an `EndPoint` in `SocketConnectionListener.Bind` to `SocketTransportOptions`. If `SocketTransportOptions.CreateListenSocket` is set then it is used in preference of `SocketTransportOptions.CreateDefaultListenSocket` and it is expected that the function creates the right type of socket for the passed `EndPoint`. Implementors can call `SocketTransportOptions.CreateDefaultListenSocket` themselves and manipulate the returned socket instance as they see fit. Note that during implementation I removed the `_socketHandle` field from `SocketConnectionListener` - this was only set so that `Dispose` could be called when the listener is disposed. Under the hood `Socket` already disposes a handle passed to it during finalization, but only if the `ownsHandle` parameter is `true` . In this case the `SafeSocketHandle` _is_ instantiated with this parameter so the the underlying handle will be closed when the `_listenSocket` field is disposed - that is currently the case when the listener is disposed. --- .../src/PublicAPI.Unshipped.txt | 7 +- .../src/SocketConnectionListener.cs | 48 ++--------- .../src/SocketTransportOptions.cs | 40 ++++++++- .../SocketTransportOptionsTests.cs | 81 +++++++------------ 4 files changed, 75 insertions(+), 101 deletions(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index 2c5391731922..b45158770ed3 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -7,7 +7,6 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.Bin ~Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptedSocket.get -> System.Action? -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureAcceptedSocket.set -> void -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureListenSocket.get -> System.Action? -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.ConfigureListenSocket.set -> void \ No newline at end of file +static Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultListenSocket(System.Net.EndPoint! endpoint) -> System.Net.Sockets.Socket! +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.get -> System.Func? +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.set -> void \ No newline at end of file diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index d9cf4991389b..65512124c632 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -23,7 +23,6 @@ internal sealed class SocketConnectionListener : IConnectionListener private Socket? _listenSocket; private int _settingsIndex; private readonly SocketTransportOptions _options; - private SafeSocketHandle? _socketHandle; public EndPoint EndPoint { get; private set; } @@ -91,42 +90,11 @@ internal void Bind() throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); } - Socket listenSocket; - - switch (EndPoint) - { - case FileHandleEndPoint fileHandle: - _socketHandle = new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true); - listenSocket = new Socket(_socketHandle); - ConfigureSocket(); - break; - case UnixDomainSocketEndPoint unix: - listenSocket = new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified); - ConfigureSocket(); - BindSocket(); - break; - case IPEndPoint ip: - listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 - if (ip.Address == IPAddress.IPv6Any) - { - listenSocket.DualMode = true; - } - - ConfigureSocket(); - BindSocket(); - break; - default: - listenSocket = new Socket(EndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - ConfigureSocket(); - BindSocket(); - break; - } - - void ConfigureSocket() => _options.ConfigureListenSocket?.Invoke(EndPoint, listenSocket); - - void BindSocket() + var listenSocketFactory = _options.CreateListenSocket ?? SocketTransportOptions.CreateDefaultListenSocket; + var listenSocket = listenSocketFactory(EndPoint); + // we only call Bind on sockets that were _not_ created + // using a file handle, otherwise underlying PAL call throws + if (!(EndPoint is FileHandleEndPoint)) { try { @@ -162,8 +130,6 @@ void BindSocket() acceptSocket.NoDelay = _options.NoDelay; } - _options.ConfigureAcceptedSocket?.Invoke(EndPoint, acceptSocket); - var setting = _settings[_settingsIndex]; var connection = new SocketConnection(acceptSocket, @@ -202,8 +168,6 @@ void BindSocket() public ValueTask UnbindAsync(CancellationToken cancellationToken = default) { _listenSocket?.Dispose(); - - _socketHandle?.Dispose(); return default; } @@ -211,8 +175,6 @@ public ValueTask DisposeAsync() { _listenSocket?.Dispose(); - _socketHandle?.Dispose(); - // Dispose the memory pool _memoryPool.Dispose(); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index f4f0446e3943..97c36cbecda8 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Net; using System.Net.Sockets; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets { @@ -68,14 +69,45 @@ public class SocketTransportOptions public bool UnsafePreferInlineScheduling { get; set; } /// - /// An action used to configure the listening socket before it is bound. + /// A function used to create a new to listen with. If + /// not set, is called instead. /// - public Action? ConfigureListenSocket { get; set; } + public Func? CreateListenSocket { get; set; } /// - /// An action used to configure an accepted socket before it's used. + /// Creates a default instance of for the given + /// that can be used by a connection listener to listen for inbound requests. /// - public Action? ConfigureAcceptedSocket { get; set; } + /// + /// An . + /// + /// + /// A instance. + /// + public static Socket CreateDefaultListenSocket(EndPoint endpoint) + { + switch (endpoint) + { + case FileHandleEndPoint fileHandle: + return new Socket( + new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true) + ); + case UnixDomainSocketEndPoint unix: + return new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified); + case IPEndPoint ip: + var listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 + if (ip.Address == IPAddress.IPv6Any) + { + listenSocket.DualMode = true; + } + + return listenSocket; + default: + return new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + } + } internal Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; } diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs index 8d275963ae7a..ad479c2c1c70 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs @@ -1,21 +1,20 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Sockets; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Hosting; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Sockets.BindTests { public class SocketTransportOptionsTests : LoggedTestBase { @@ -24,36 +23,32 @@ public class SocketTransportOptionsTests : LoggedTestBase public async Task SocketTransportCallsConfigureListenSocket(EndPoint endpointToTest) { var wasCalled = false; - void ConfigureListenSocket(EndPoint endpoint, Socket socket) => wasCalled = true; - using var host = CreateWebHost( - endpointToTest, options => options.ConfigureListenSocket = ConfigureListenSocket - ); - await host.StartAsync(); - Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.ConfigureListenSocket)} to be called."); - await host.StopAsync(); - } - - [Theory] - [MemberData(nameof(GetEndpoints))] - public async Task SocketTransportCallsConfigureAcceptedSocket(EndPoint endpointToTest) - { - var wasCalled = false; - void ConfigureAcceptedSocket(EndPoint endpoint, Socket socket) => wasCalled = true; + Socket CreateListenSocket(EndPoint endpoint) + { + wasCalled = true; + return SocketTransportOptions.CreateDefaultListenSocket(endpoint); + } using var host = CreateWebHost( - endpointToTest, options => options.ConfigureAcceptedSocket = ConfigureAcceptedSocket + endpointToTest, + options => + { + options.CreateListenSocket = CreateListenSocket; + } ); - using var client = CreateHttpClient(endpointToTest); + await host.StartAsync(); - var uri = host.GetUris().First(); - var response = await client.GetAsync(uri); - response.EnsureSuccessStatusCode(); - Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.ConfigureAcceptedSocket)} to be called."); + Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.CreateListenSocket)} to be called."); await host.StopAsync(); } - private static int _counter = 1; + // static to ensure that the underlying handle doesn't get closed + // when our underlying connection listener is disposed + private static readonly Socket _fileHandleSocket = new( + AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp + ); + public static IEnumerable GetEndpoints() { // IPv4 @@ -65,15 +60,19 @@ public static IEnumerable GetEndpoints() { yield return new object[] { - // NOTE - // to avoid "address in use" errors need to ensure - // the socket path is unique - new UnixDomainSocketEndPoint( - $"/tmp/test-{Interlocked.Increment(ref _counter)}.sock" - ) + new UnixDomainSocketEndPoint($"/tmp/test.sock") }; } + // file handle + // slightly messy but allows us to create a FileHandleEndPoint + // from the underlying OS handle used by the socket + _fileHandleSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + yield return new object[] + { + new FileHandleEndPoint((ulong) _fileHandleSocket.Handle, FileHandleType.Auto) + }; + // TODO: other endpoint types? } @@ -92,23 +91,5 @@ private IHost CreateWebHost(EndPoint endpoint, Action co ) .ConfigureServices(AddTestLogging) .Build(); - private static HttpClient CreateHttpClient(EndPoint endpoint) - { - if (endpoint is UnixDomainSocketEndPoint) - { - // https://stackoverflow.com/a/67203488/871146 - return new HttpClient(new SocketsHttpHandler - { - ConnectCallback = async (_, _) => - { - var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - await socket.ConnectAsync(endpoint); - return new NetworkStream(socket, ownsSocket: true); - } - }); - } - - return new HttpClient(); - } } } From 99676bb40f77b2efc35bddffc87856ea54daa203 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Thu, 20 May 2021 22:59:02 +0100 Subject: [PATCH 6/9] Make `CreateListenSocket` non-nullable and initialize to `CreateDefaultListenSocket` --- .../Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt | 2 +- .../Transport.Sockets/src/SocketConnectionListener.cs | 5 ++--- .../Kestrel/Transport.Sockets/src/SocketTransportOptions.cs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index b45158770ed3..0612cd217153 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -8,5 +8,5 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.Bin static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultListenSocket(System.Net.EndPoint! endpoint) -> System.Net.Sockets.Socket! -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.get -> System.Func? +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.get -> System.Func! Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.set -> void \ No newline at end of file diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index 65512124c632..64b9e7a9593d 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -89,9 +89,8 @@ internal void Bind() { throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); } - - var listenSocketFactory = _options.CreateListenSocket ?? SocketTransportOptions.CreateDefaultListenSocket; - var listenSocket = listenSocketFactory(EndPoint); + + var listenSocket = _options.CreateListenSocket(EndPoint); // we only call Bind on sockets that were _not_ created // using a file handle, otherwise underlying PAL call throws if (!(EndPoint is FileHandleEndPoint)) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 97c36cbecda8..78936d5e0027 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -70,9 +70,9 @@ public class SocketTransportOptions /// /// A function used to create a new to listen with. If - /// not set, is called instead. + /// not set, is used. /// - public Func? CreateListenSocket { get; set; } + public Func CreateListenSocket { get; set; } = CreateDefaultListenSocket; /// /// Creates a default instance of for the given From 5374ff4a118a4d7801b02ce30d1a1e8e4b3f5fec Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Thu, 20 May 2021 22:59:31 +0100 Subject: [PATCH 7/9] Update test to use a time-based path for `UnixDomainSocketEndPoint` --- .../test/Sockets.BindTests/SocketTransportOptionsTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs index ad479c2c1c70..5db2b673a866 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; @@ -60,7 +61,7 @@ public static IEnumerable GetEndpoints() { yield return new object[] { - new UnixDomainSocketEndPoint($"/tmp/test.sock") + new UnixDomainSocketEndPoint($"/tmp/{DateTime.UtcNow:yyyyMMddTHHmmss}.sock") }; } From 15497a1716153612261d57c0b788775dc7592388 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Thu, 20 May 2021 23:55:34 +0100 Subject: [PATCH 8/9] Add clarifying comment --- .../Transport.Sockets/src/SocketTransportOptions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 78936d5e0027..ab7ef95e96ee 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -89,6 +89,15 @@ public static Socket CreateDefaultListenSocket(EndPoint endpoint) switch (endpoint) { case FileHandleEndPoint fileHandle: + // We're passing "ownsHandle: true" here even though we don't necessarily + // own the handle because Socket.Dispose will clean-up everything safely. + // If the handle was already closed or disposed then the socket will + // be torn down gracefully, and if the caller never cleans up their handle + // then we'll do it for them. + // + // If we don't do this then we run the risk of Kestrel hanging because the + // the underlying socket is never closed and the transport manager can hang + // when it attempts to stop. return new Socket( new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true) ); From f4a38b7461912803b5295152df14ec7d62100751 Mon Sep 17 00:00:00 2001 From: Dean Ward Date: Thu, 27 May 2021 15:55:46 +0100 Subject: [PATCH 9/9] Tweak to match approved API - `CreateListenSocket` => `CreateBoundListenSocket` Moves the call to `Socket.Bind` from the `SocketConnectionListener` to `SocketTransportOptions` and adds xmldoc detailing behaviour. Also added additional comments and another test to validate the behaviour of `CreateDefaultBoundListenSocket` using different kinds of endpoints. --- .../src/PublicAPI.Unshipped.txt | 6 +-- .../src/SocketConnectionListener.cs | 22 +++++----- .../src/SocketTransportOptions.cs | 40 ++++++++++++++----- .../SocketTransportOptionsTests.cs | 30 +++++++++----- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index 0612cd217153..5708e0985dfd 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -7,6 +7,6 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.Bin ~Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! -static Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultListenSocket(System.Net.EndPoint! endpoint) -> System.Net.Sockets.Socket! -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.get -> System.Func! -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateListenSocket.set -> void \ No newline at end of file +static Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(System.Net.EndPoint! endpoint) -> System.Net.Sockets.Socket! +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateBoundListenSocket.get -> System.Func! +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateBoundListenSocket.set -> void \ No newline at end of file diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index 64b9e7a9593d..43e5f19172bc 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.ComponentModel; using System.Diagnostics; using System.IO.Pipelines; using System.Net; @@ -89,20 +90,15 @@ internal void Bind() { throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); } - - var listenSocket = _options.CreateListenSocket(EndPoint); - // we only call Bind on sockets that were _not_ created - // using a file handle, otherwise underlying PAL call throws - if (!(EndPoint is FileHandleEndPoint)) + + Socket listenSocket; + try { - try - { - listenSocket.Bind(EndPoint); - } - catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) - { - throw new AddressInUseException(e.Message, e); - } + listenSocket = _options.CreateBoundListenSocket(EndPoint); + } + catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) + { + throw new AddressInUseException(e.Message, e); } Debug.Assert(listenSocket.LocalEndPoint != null); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index ab7ef95e96ee..6e2cb7ca4735 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -70,13 +70,20 @@ public class SocketTransportOptions /// /// A function used to create a new to listen with. If - /// not set, is used. + /// not set, is used. /// - public Func CreateListenSocket { get; set; } = CreateDefaultListenSocket; + /// + /// Implementors are expected to call on the + /// . Please note that + /// calls as part of its implementation, so implementors + /// using this method do not need to call it again. + /// + public Func CreateBoundListenSocket { get; set; } = CreateDefaultBoundListenSocket; /// /// Creates a default instance of for the given - /// that can be used by a connection listener to listen for inbound requests. + /// that can be used by a connection listener to listen for inbound requests. + /// is called by this method. /// /// /// An . @@ -84,8 +91,9 @@ public class SocketTransportOptions /// /// A instance. /// - public static Socket CreateDefaultListenSocket(EndPoint endpoint) + public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint) { + Socket listenSocket; switch (endpoint) { case FileHandleEndPoint fileHandle: @@ -98,13 +106,15 @@ public static Socket CreateDefaultListenSocket(EndPoint endpoint) // If we don't do this then we run the risk of Kestrel hanging because the // the underlying socket is never closed and the transport manager can hang // when it attempts to stop. - return new Socket( + listenSocket = new Socket( new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true) ); + break; case UnixDomainSocketEndPoint unix: - return new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified); + listenSocket = new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified); + break; case IPEndPoint ip: - var listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 if (ip.Address == IPAddress.IPv6Any) @@ -112,10 +122,22 @@ public static Socket CreateDefaultListenSocket(EndPoint endpoint) listenSocket.DualMode = true; } - return listenSocket; + break; default: - return new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + listenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + break; + } + + // we only call Bind on sockets that were _not_ created + // using a file handle; the handle is already bound + // to an underlying socket so doing it again causes the + // underlying PAL call to throw + if (!(endpoint is FileHandleEndPoint)) + { + listenSocket.Bind(endpoint); } + + return listenSocket; } internal Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs index 5db2b673a866..b094510bfde1 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportOptionsTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; @@ -21,34 +20,40 @@ public class SocketTransportOptionsTests : LoggedTestBase { [Theory] [MemberData(nameof(GetEndpoints))] - public async Task SocketTransportCallsConfigureListenSocket(EndPoint endpointToTest) + public async Task SocketTransportCallsCreateBoundListenSocket(EndPoint endpointToTest) { var wasCalled = false; Socket CreateListenSocket(EndPoint endpoint) { wasCalled = true; - return SocketTransportOptions.CreateDefaultListenSocket(endpoint); + return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); } using var host = CreateWebHost( endpointToTest, options => { - options.CreateListenSocket = CreateListenSocket; + options.CreateBoundListenSocket = CreateListenSocket; } ); await host.StartAsync(); - Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.CreateListenSocket)} to be called."); + Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.CreateBoundListenSocket)} to be called."); await host.StopAsync(); } - // static to ensure that the underlying handle doesn't get closed - // when our underlying connection listener is disposed - private static readonly Socket _fileHandleSocket = new( - AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp - ); + [Theory] + [MemberData(nameof(GetEndpoints))] + public void CreateDefaultBoundListenSocket_BindsForAllEndPoints(EndPoint endpoint) + { + using var listenSocket = SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); + Assert.NotNull(listenSocket.LocalEndPoint); + } + + // static to ensure that the underlying handle doesn't get disposed + // when a local reference is GCed by the iterator in GetEndPoints + private static Socket _fileHandleSocket; public static IEnumerable GetEndpoints() { @@ -61,13 +66,16 @@ public static IEnumerable GetEndpoints() { yield return new object[] { - new UnixDomainSocketEndPoint($"/tmp/{DateTime.UtcNow:yyyyMMddTHHmmss}.sock") + new UnixDomainSocketEndPoint($"/tmp/{DateTime.UtcNow:yyyyMMddTHHmmss.fff}.sock") }; } // file handle // slightly messy but allows us to create a FileHandleEndPoint // from the underlying OS handle used by the socket + _fileHandleSocket = new( + AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp + ); _fileHandleSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); yield return new object[] {