Skip to content

Commit 3341e46

Browse files
deanward81halter73
andauthored
Add support for customising the creation of Kestrel listen sockets (#32827)
* 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<EndPoint, Socket>? ConfigureListenSocket { get; set; }` which allows the listen socket to be configured. - `Action<EndPoint, Socket>? 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. * `ConfigureAcceptSocket` => `ConfigureAcceptedSocket` * Update public API bits * Update src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs Co-authored-by: Stephen Halter <halter73@gmail.com> * 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. * Make `CreateListenSocket` non-nullable and initialize to `CreateDefaultListenSocket` * Update test to use a time-based path for `UnixDomainSocketEndPoint` * Add clarifying comment * 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. Co-authored-by: Stephen Halter <halter73@gmail.com>
1 parent ac81287 commit 3341e46

File tree

4 files changed

+187
-39
lines changed

4 files changed

+187
-39
lines changed

src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.Bin
77
~Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions!>! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void
88
static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
99
static Microsoft.AspNetCore.Hosting.WebHostBuilderSocketExtensions.UseSockets(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action<Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions!>! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
10+
static Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateDefaultBoundListenSocket(System.Net.EndPoint! endpoint) -> System.Net.Sockets.Socket!
11+
Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateBoundListenSocket.get -> System.Func<System.Net.EndPoint!, System.Net.Sockets.Socket!>!
12+
Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportOptions.CreateBoundListenSocket.set -> void

src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Buffers;
6+
using System.ComponentModel;
67
using System.Diagnostics;
78
using System.IO.Pipelines;
89
using System.Net;
@@ -23,7 +24,6 @@ internal sealed class SocketConnectionListener : IConnectionListener
2324
private Socket? _listenSocket;
2425
private int _settingsIndex;
2526
private readonly SocketTransportOptions _options;
26-
private SafeSocketHandle? _socketHandle;
2727

2828
public EndPoint EndPoint { get; private set; }
2929

@@ -92,43 +92,13 @@ internal void Bind()
9292
}
9393

9494
Socket listenSocket;
95-
96-
switch (EndPoint)
95+
try
9796
{
98-
case FileHandleEndPoint fileHandle:
99-
_socketHandle = new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true);
100-
listenSocket = new Socket(_socketHandle);
101-
break;
102-
case UnixDomainSocketEndPoint unix:
103-
listenSocket = new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified);
104-
BindSocket();
105-
break;
106-
case IPEndPoint ip:
107-
listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
108-
109-
// Kestrel expects IPv6Any to bind to both IPv6 and IPv4
110-
if (ip.Address == IPAddress.IPv6Any)
111-
{
112-
listenSocket.DualMode = true;
113-
}
114-
BindSocket();
115-
break;
116-
default:
117-
listenSocket = new Socket(EndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
118-
BindSocket();
119-
break;
97+
listenSocket = _options.CreateBoundListenSocket(EndPoint);
12098
}
121-
122-
void BindSocket()
99+
catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
123100
{
124-
try
125-
{
126-
listenSocket.Bind(EndPoint);
127-
}
128-
catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
129-
{
130-
throw new AddressInUseException(e.Message, e);
131-
}
101+
throw new AddressInUseException(e.Message, e);
132102
}
133103

134104
Debug.Assert(listenSocket.LocalEndPoint != null);
@@ -193,17 +163,13 @@ void BindSocket()
193163
public ValueTask UnbindAsync(CancellationToken cancellationToken = default)
194164
{
195165
_listenSocket?.Dispose();
196-
197-
_socketHandle?.Dispose();
198166
return default;
199167
}
200168

201169
public ValueTask DisposeAsync()
202170
{
203171
_listenSocket?.Dispose();
204172

205-
_socketHandle?.Dispose();
206-
207173
// Dispose the memory pool
208174
_memoryPool.Dispose();
209175

src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Net;
7+
using System.Net.Sockets;
8+
using Microsoft.AspNetCore.Connections;
69

710
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
811
{
@@ -65,6 +68,78 @@ public class SocketTransportOptions
6568
/// </remarks>
6669
public bool UnsafePreferInlineScheduling { get; set; }
6770

71+
/// <summary>
72+
/// A function used to create a new <see cref="Socket"/> to listen with. If
73+
/// not set, <see cref="CreateDefaultBoundListenSocket" /> is used.
74+
/// </summary>
75+
/// <remarks>
76+
/// Implementors are expected to call <see cref="Socket.Bind"/> on the
77+
/// <see cref="Socket"/>. Please note that <see cref="CreateDefaultBoundListenSocket"/>
78+
/// calls <see cref="Socket.Bind"/> as part of its implementation, so implementors
79+
/// using this method do not need to call it again.
80+
/// </remarks>
81+
public Func<EndPoint, Socket> CreateBoundListenSocket { get; set; } = CreateDefaultBoundListenSocket;
82+
83+
/// <summary>
84+
/// Creates a default instance of <see cref="Socket"/> for the given <see cref="EndPoint"/>
85+
/// that can be used by a connection listener to listen for inbound requests. <see cref="Socket.Bind"/>
86+
/// is called by this method.
87+
/// </summary>
88+
/// <param name="endpoint">
89+
/// An <see cref="EndPoint"/>.
90+
/// </param>
91+
/// <returns>
92+
/// A <see cref="Socket"/> instance.
93+
/// </returns>
94+
public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint)
95+
{
96+
Socket listenSocket;
97+
switch (endpoint)
98+
{
99+
case FileHandleEndPoint fileHandle:
100+
// We're passing "ownsHandle: true" here even though we don't necessarily
101+
// own the handle because Socket.Dispose will clean-up everything safely.
102+
// If the handle was already closed or disposed then the socket will
103+
// be torn down gracefully, and if the caller never cleans up their handle
104+
// then we'll do it for them.
105+
//
106+
// If we don't do this then we run the risk of Kestrel hanging because the
107+
// the underlying socket is never closed and the transport manager can hang
108+
// when it attempts to stop.
109+
listenSocket = new Socket(
110+
new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true)
111+
);
112+
break;
113+
case UnixDomainSocketEndPoint unix:
114+
listenSocket = new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified);
115+
break;
116+
case IPEndPoint ip:
117+
listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
118+
119+
// Kestrel expects IPv6Any to bind to both IPv6 and IPv4
120+
if (ip.Address == IPAddress.IPv6Any)
121+
{
122+
listenSocket.DualMode = true;
123+
}
124+
125+
break;
126+
default:
127+
listenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
128+
break;
129+
}
130+
131+
// we only call Bind on sockets that were _not_ created
132+
// using a file handle; the handle is already bound
133+
// to an underlying socket so doing it again causes the
134+
// underlying PAL call to throw
135+
if (!(endpoint is FileHandleEndPoint))
136+
{
137+
listenSocket.Bind(endpoint);
138+
}
139+
140+
return listenSocket;
141+
}
142+
68143
internal Func<MemoryPool<byte>> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create;
69144
}
70145
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net;
4+
using System.Net.Sockets;
5+
using System.Runtime.InteropServices;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.AspNetCore.Connections;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
12+
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
13+
using Microsoft.AspNetCore.Testing;
14+
using Microsoft.Extensions.Hosting;
15+
using Xunit;
16+
17+
namespace Sockets.BindTests
18+
{
19+
public class SocketTransportOptionsTests : LoggedTestBase
20+
{
21+
[Theory]
22+
[MemberData(nameof(GetEndpoints))]
23+
public async Task SocketTransportCallsCreateBoundListenSocket(EndPoint endpointToTest)
24+
{
25+
var wasCalled = false;
26+
27+
Socket CreateListenSocket(EndPoint endpoint)
28+
{
29+
wasCalled = true;
30+
return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint);
31+
}
32+
33+
using var host = CreateWebHost(
34+
endpointToTest,
35+
options =>
36+
{
37+
options.CreateBoundListenSocket = CreateListenSocket;
38+
}
39+
);
40+
41+
await host.StartAsync();
42+
Assert.True(wasCalled, $"Expected {nameof(SocketTransportOptions.CreateBoundListenSocket)} to be called.");
43+
await host.StopAsync();
44+
}
45+
46+
[Theory]
47+
[MemberData(nameof(GetEndpoints))]
48+
public void CreateDefaultBoundListenSocket_BindsForAllEndPoints(EndPoint endpoint)
49+
{
50+
using var listenSocket = SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint);
51+
Assert.NotNull(listenSocket.LocalEndPoint);
52+
}
53+
54+
// static to ensure that the underlying handle doesn't get disposed
55+
// when a local reference is GCed by the iterator in GetEndPoints
56+
private static Socket _fileHandleSocket;
57+
58+
public static IEnumerable<object[]> GetEndpoints()
59+
{
60+
// IPv4
61+
yield return new object[] {new IPEndPoint(IPAddress.Loopback, 0)};
62+
// IPv6
63+
yield return new object[] {new IPEndPoint(IPAddress.IPv6Loopback, 0)};
64+
// Unix sockets
65+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
66+
{
67+
yield return new object[]
68+
{
69+
new UnixDomainSocketEndPoint($"/tmp/{DateTime.UtcNow:yyyyMMddTHHmmss.fff}.sock")
70+
};
71+
}
72+
73+
// file handle
74+
// slightly messy but allows us to create a FileHandleEndPoint
75+
// from the underlying OS handle used by the socket
76+
_fileHandleSocket = new(
77+
AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp
78+
);
79+
_fileHandleSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
80+
yield return new object[]
81+
{
82+
new FileHandleEndPoint((ulong) _fileHandleSocket.Handle, FileHandleType.Auto)
83+
};
84+
85+
// TODO: other endpoint types?
86+
}
87+
88+
private IHost CreateWebHost(EndPoint endpoint, Action<SocketTransportOptions> configureSocketOptions) =>
89+
TransportSelector.GetHostBuilder()
90+
.ConfigureWebHost(
91+
webHostBuilder =>
92+
{
93+
webHostBuilder
94+
.UseSockets(configureSocketOptions)
95+
.UseKestrel(options => options.Listen(endpoint))
96+
.Configure(
97+
app => app.Run(ctx => ctx.Response.WriteAsync("Hello World"))
98+
);
99+
}
100+
)
101+
.ConfigureServices(AddTestLogging)
102+
.Build();
103+
}
104+
}

0 commit comments

Comments
 (0)