Skip to content

Add support for customising the creation of Kestrel listen sockets #32827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 28, 2021
Merged

Add support for customising the creation of Kestrel listen sockets #32827

merged 9 commits into from
May 28, 2021

Conversation

deanward81
Copy link
Contributor

@deanward81 deanward81 commented May 18, 2021

Fixes #32794. As mentioned in the issue it is currently not possible to configure the underlying listen socket used by Kestrel. In certain circumstances it is desirable to be able to configure socket options when the socket is created - 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 (more details on that specific case here).

This change adds the API suggested by #32794 by adding a function that allows the caller to manage the creation of the listening socket - Func<EndPoint, Socket> CreateListenSocket { get; set; } allows the creation of the listen socket to be configured.

There's also some tests using IPv4, IPv6, unix domain sockets and file handles.

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.
@ghost ghost added area-runtime community-contribution Indicates that the PR has been added by a community member labels May 18, 2021
@davidfowl davidfowl added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label May 18, 2021
@ghost
Copy link

ghost commented May 18, 2021

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@deanward81 deanward81 requested a review from BrennanConroy as a code owner May 19, 2021 11:26
@halter73
Copy link
Member

halter73 commented May 19, 2021

I wonder if it wouldn't be better to change ConfigureListenSocket from Action<EndPoint, Socket> to Func<EndPoint, Socket>. If we went this route, we'd want to expose a Socket CreateListenSocket(EndPoint) method somewhere and document it with these callbacks.

@Tratcher
Copy link
Member

Create seems like a different scenario from augmentation, and requires more from the implementer.

@@ -111,14 +113,19 @@ internal void Bind()
{
listenSocket.DualMode = true;
}

ConfigureSocket();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go the Action<EndPoint, Socket> route, wouldn't it be better to call it after binding but before listening? This would make the FileHandleEndPoint and IPEndPoint cases more similar.

If we go the Func<EndPoint, Socket> route, I'd expect the returned Socket to already be bound.

@halter73
Copy link
Member

and requires more from the implementer.

Not really given Socket CreateListenSocket(EndPoint) exists.

@deanward81
Copy link
Contributor Author

and requires more from the implementer.

Not really given Socket CreateListenSocket(EndPoint) exists.

Are you saying that this method would exist when moving to a Func? I can’t see anything existing like this method except CreateListenSocket in libuv that returns a UvStreamHandle:

private UvStreamHandle CreateListenSocket()

…ns.cs

Co-authored-by: Stephen Halter <halter73@gmail.com>
@halter73
Copy link
Member

halter73 commented May 19, 2021

New proposed API discussed in triage:

public class SocketTransportOptions
{
+    public Func<EndPoint, Socket> CreateListenSocket { get; set; }
+    public static Socket CreateDefaultListenSocket(EndPoint);
}

We don't think ConfigureAcceptedSocket adds much value. We recommend adding connection middleware and using the new IConnectionSocketFeature (#31588).

@davidfowl
Copy link
Member

@halter73 how do you "call base" for the listen socket.

@Tratcher
Copy link
Member

Is CreateDefaultListenSocket supposed to be static?

@davidfowl
Copy link
Member

Yea it should be the stateless default.

@halter73
Copy link
Member

halter73 commented May 20, 2021

Yeah. It should be static. I think it would basically be the current implementation SocketConnectionListen.Bind(), but instead of calling Socket.Listen(int) and setting _listenSocket, it would return the Socket before listening. The new implementation of Bind() would then call the SocketTransportOptions.CreateListenSocket(EndPoint) Func and then call Socket.Listen(int) on the returned value and set it to _listenSocket.

We might have to bypass the CreateListenSocket for FileHandleEndPoints since I can't think of a great way to manage the _socketHandle in that case. I think this is fine because if you're using a FileHandleEndPoint, you can configure the socket options before passing it to KestrelServerOptions.ListenHandle(long).

@halter73
Copy link
Member

halter73 commented May 20, 2021

If we make it non-static, we could do some trickery where the SocketConnectionListener adds an internal reference to itself to SocketTransportOptions before calling the CreateDefaultListenSocket callback allowing CreateDefaultListenSocket to set _socketHandle if need be. Not sure if this would be worthwhile, but I do like that it makes it work in all cases.

We could also make CreateListenSocket a Func<EndPoint, Func<EndPoint, Socket>, Socket> and do away with CreateDefaultListenSocket.

@deanward81
Copy link
Contributor Author

We might have to bypass the CreateListenSocket for FileHandleEndPoints since I can't think of a great way to manage the _socketHandle in that case. I think this is fine because if you're using a FileHandleEndPoint, you can configure the socket options before passing it to KestrelServerOptions.ListenHandle(long).

Does the _socketHandle field need to be there? It's only used during disposal, but that handle is created with ownsHandle: true which, when passed to Socket, means the handle is cleaned up in the finaliser of the socket (which arguably is more correct because it's an unmanaged resource). https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262

That said, is the handle really owned in this case? The user is passing an opaque handle created elsewhere - is it intentional that it is marked as "owned" by the socket transport? Shouldn't they be responsible for cleanup?

Reason I ask - not having that field allows CreateDefaultListenSocket to work for all cases and keeps things decoupled (no need to do mess with references to the SocketConnectionListener in the instance case).

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.
@deanward81
Copy link
Contributor Author

deanward81 commented May 20, 2021

I've updated to use the triaged API (using a static CreateDefaultListenSocket). I removed the _socketHandle in the last commit and, so far as I can tell, there are no negative consequences (tests pass, at least). Details (from the commit):

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 or unbound.

@halter73
Copy link
Member

halter73 commented May 20, 2021

New new proposed API based on #32827 (comment) 😆

public class SocketTransportOptions
{
+    public Func<EndPoint, Func<EndPoint, Socket>, Socket> CreateListenSocket { get; set; }
}

Edit: never mind

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now see that the SafeSocketHandle is disposed by Socket all along because we were specifying ownsHandle: true. This means the previous manual tracking of the SafeSocketHandle was redundant and there's no behavioral change. Or at least that's what it seems like. I'll step through the code to confirm before merging.

@deanward81 deanward81 changed the title Add support for configuring Kestrel listen and accept sockets Add support for customising the creation of Kestrel listen sockets May 20, 2021
@JamesNK
Copy link
Member

JamesNK commented May 20, 2021

@halter73 Static method is new as well for API review?

@halter73 halter73 added the * NO MERGE * Do not merge this PR as long as this label is present. label May 21, 2021
@halter73
Copy link
Member

Yes. This is still api-ready-for-review. We shouldn't merge before it's approved.

@BrennanConroy
Copy link
Member

BrennanConroy commented May 24, 2021

API review:

public class SocketTransportOptions
{
+     public static Socket CreateDefaultBoundListenSocket(EndPoint);
+     public Func<EndPoint, Socket> CreateBoundListenSocket { get; set; }
}

@BrennanConroy BrennanConroy added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels May 24, 2021
@deanward81
Copy link
Contributor Author

deanward81 commented May 24, 2021

@BrennanConroy currently the implementation of CreateDefaultListenSocket does not call Bind - the connection listener does it - with the approved API it implies that it should be called - presumably the implementation should be changed to do so?

…tenSocket`

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.
@deanward81
Copy link
Contributor Author

I've updated the code to match the API review. Let me know if you want any additional tweaks @BrennanConroy

@davidfowl
Copy link
Member

@BrennanConroy @halter73 is this good?

@halter73 halter73 merged commit 3341e46 into dotnet:main May 28, 2021
@ghost ghost added this to the 6.0-preview6 milestone May 28, 2021
@halter73
Copy link
Member

Thanks @deanward81!

@deanward81 deanward81 deleted the add-listen-accept-socket-configuration branch June 5, 2021 09:55
@sebastienros
Copy link
Member

Did we measure the perf impact of that change? Asking for a friend :/

@halter73
Copy link
Member

@sebastienros We didn't measure the perf of this change, but I'd be shocked if this had a perf impact. This moves the creation of the listen socket (not the accepted socket) to a Func, but keeps the logic the same by default. Even if there was a perf hit from calling a Func, it's only called once per address at app startup.

@halter73
Copy link
Member

#34769 would be more likely to affect Connection: close perf because it touches a per-connection code path. I don't think we explicitly perf tested that. However, it's basically just a refactoring, so it shouldn't impact perf.

@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions community-contribution Indicates that the PR has been added by a community member * NO MERGE * Do not merge this PR as long as this label is present.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Configure the Accept Socket
8 participants