diff --git a/src/Servers/Connections.Abstractions/src/Features/IConnectionSocketFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IConnectionSocketFeature.cs
new file mode 100644
index 000000000000..1b0a52507b6d
--- /dev/null
+++ b/src/Servers/Connections.Abstractions/src/Features/IConnectionSocketFeature.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Net.Sockets;
+
+namespace Microsoft.AspNetCore.Connections.Features
+{
+ ///
+ /// Provides access to the connection's underlying .
+ ///
+ public interface IConnectionSocketFeature
+ {
+ ///
+ /// Gets the underlying .
+ ///
+ Socket Socket { get; }
+ }
+}
diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
index 575c538f6a9b..9d21a5e74882 100644
--- a/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
@@ -1,5 +1,7 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature
+Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature.Socket.get -> System.Net.Sockets.Socket!
Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
Microsoft.AspNetCore.Connections.Experimental.IMultiplexedConnectionBuilder
Microsoft.AspNetCore.Connections.Experimental.IMultiplexedConnectionBuilder.ApplicationServices.get -> System.IServiceProvider!
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs
new file mode 100644
index 000000000000..8645f7e6459c
--- /dev/null
+++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Net.Sockets;
+using Microsoft.AspNetCore.Connections.Features;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
+{
+ internal sealed partial class SocketConnection : IConnectionSocketFeature
+ {
+ public Socket Socket => _socket;
+
+ private void InitiaizeFeatures()
+ {
+ _currentIConnectionSocketFeature = this;
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
index 49f6c24f6446..35b7c66113ec 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
@@ -13,7 +13,7 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
{
- internal sealed class SocketConnection : TransportConnection
+ internal sealed partial class SocketConnection : TransportConnection
{
private static readonly int MinAllocBufferSize = SlabMemoryPool.BlockSize / 2;
@@ -70,6 +70,8 @@ internal SocketConnection(Socket socket,
// Set the transport and connection id
Transport = _originalTransport = pair.Transport;
Application = pair.Application;
+
+ InitiaizeFeatures();
}
public PipeWriter Input => Application.Output;
diff --git a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
index 0f32b7d54549..18b69ce0e46d 100644
--- a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
+++ b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
@@ -27,6 +27,9 @@ internal partial class TransportConnection : IFeatureCollection,
internal protected IMemoryPoolFeature? _currentIMemoryPoolFeature;
internal protected IConnectionLifetimeFeature? _currentIConnectionLifetimeFeature;
+ // Other reserved feature slots
+ internal protected IConnectionSocketFeature? _currentIConnectionSocketFeature;
+
private int _featureRevision;
private List>? MaybeExtra;
@@ -39,6 +42,7 @@ private void FastReset()
_currentIMemoryPoolFeature = this;
_currentIConnectionLifetimeFeature = this;
+ _currentIConnectionSocketFeature = null;
}
// Internal for testing
@@ -130,6 +134,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
feature = _currentIConnectionLifetimeFeature;
}
+ else if (key == typeof(IConnectionSocketFeature))
+ {
+ feature = _currentIConnectionSocketFeature;
+ }
else if (MaybeExtra != null)
{
feature = ExtraFeatureGet(key);
@@ -162,6 +170,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
_currentIConnectionLifetimeFeature = (IConnectionLifetimeFeature?)value;
}
+ else if (key == typeof(IConnectionSocketFeature))
+ {
+ _currentIConnectionSocketFeature = (IConnectionSocketFeature?)value;
+ }
else
{
ExtraFeatureSet(key, value);
@@ -196,6 +208,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
feature = Unsafe.As(ref _currentIConnectionLifetimeFeature);
}
+ else if (typeof(TFeature) == typeof(IConnectionSocketFeature))
+ {
+ feature = Unsafe.As(ref _currentIConnectionSocketFeature);
+ }
else if (MaybeExtra != null)
{
feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature)));
@@ -231,6 +247,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
_currentIConnectionLifetimeFeature = Unsafe.As(ref feature);
}
+ else if (typeof(TFeature) == typeof(IConnectionSocketFeature))
+ {
+ _currentIConnectionSocketFeature = Unsafe.As(ref feature);
+ }
else
{
ExtraFeatureSet(typeof(TFeature), feature);
@@ -259,6 +279,10 @@ private IEnumerable> FastEnumerable()
{
yield return new KeyValuePair(typeof(IConnectionLifetimeFeature), _currentIConnectionLifetimeFeature);
}
+ if (_currentIConnectionSocketFeature != null)
+ {
+ yield return new KeyValuePair(typeof(IConnectionSocketFeature), _currentIConnectionSocketFeature);
+ }
if (MaybeExtra != null)
{
diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/SocketTranspotTests.cs b/src/Servers/Kestrel/test/Sockets.FunctionalTests/SocketTranspotTests.cs
new file mode 100644
index 000000000000..ae619a1b7e21
--- /dev/null
+++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/SocketTranspotTests.cs
@@ -0,0 +1,54 @@
+using System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Sockets.FunctionalTests
+{
+ public class SocketTranspotTests : LoggedTestBase
+ {
+ [Fact]
+ public async Task SocketTransportExposesSocketsFeature()
+ {
+ var builder = TransportSelector.GetHostBuilder()
+ .ConfigureWebHost(webHostBuilder =>
+ {
+ webHostBuilder
+ .UseKestrel()
+ .UseUrls("http://127.0.0.1:0")
+ .Configure(app =>
+ {
+ app.Run(context =>
+ {
+ var socket = context.Features.Get().Socket;
+ Assert.NotNull(socket);
+ Assert.Equal(ProtocolType.Tcp, socket.ProtocolType);
+ var ip = (IPEndPoint)socket.RemoteEndPoint;
+ Assert.Equal(ip.Address, context.Connection.RemoteIpAddress);
+ Assert.Equal(ip.Port, context.Connection.RemotePort);
+
+ return Task.CompletedTask;
+ });
+ });
+ })
+ .ConfigureServices(AddTestLogging);
+
+ using var host = builder.Build();
+ using var client = new HttpClient();
+
+ await host.StartAsync();
+
+ var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/");
+ response.EnsureSuccessStatusCode();
+
+ await host.StopAsync();
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
index 60d00f404a2b..912615803957 100644
--- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
+++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
@@ -11,7 +11,18 @@ public static string GenerateFile()
{
// NOTE: This list MUST always match the set of feature interfaces implemented by TransportConnection.
// See also: shared/TransportConnection.FeatureCollection.cs
- var features = new[]
+
+ var allFeatures = new[]
+ {
+ "IConnectionIdFeature",
+ "IConnectionTransportFeature",
+ "IConnectionItemsFeature",
+ "IMemoryPoolFeature",
+ "IConnectionLifetimeFeature",
+ "IConnectionSocketFeature"
+ };
+
+ var implementedFeatures = new[]
{
"IConnectionIdFeature",
"IConnectionTransportFeature",
@@ -27,8 +38,8 @@ public static string GenerateFile()
return FeatureCollectionGenerator.GenerateFile(
namespaceName: "Microsoft.AspNetCore.Connections",
className: "TransportConnection",
- allFeatures: features,
- implementedFeatures: features,
+ allFeatures: allFeatures,
+ implementedFeatures: implementedFeatures,
extraUsings: usings,
fallbackFeatures: null);
}