Skip to content

Commit

Permalink
Merged PR 32090: [7.0] Forcibly close socket on abort (#49927)
Browse files Browse the repository at this point in the history
Forcibly close socket when connection is aborted.

Co-authored-by: Andrew Casey <[email protected]>
  • Loading branch information
wtgodbe and amcasey authored Aug 8, 2023
1 parent 42d7c23 commit acfe811
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 64 deletions.
7 changes: 7 additions & 0 deletions src/Servers/IIS/IIS/test/Common.LongTests/ShutdownTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 538,15 @@ public async Task ClosesConnectionOnServerAbortOutOfProcess()
var response = await deploymentResult.HttpClient.GetAsync("/Abort").TimeoutAfter(TimeoutExtensions.DefaultTimeoutValue);

Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode);

#if NEWSHIM_FUNCTIONALS
// In-proc SocketConnection isn't used and there's no abort
// 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response
Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync());
#else
// 0x80072efe ERROR_INTERNET_CONNECTION_ABORTED The connection with the server was terminated abnormally
Assert.Contains("0x80072efe", await response.Content.ReadAsStringAsync());
#endif
}
catch (HttpRequestException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 95,14 @@ protected override void OnRequestProcessingEnded()
_http1Output.Dispose();
}

public void OnInputOrOutputCompleted()
void IRequestProcessor.OnInputOrOutputCompleted()
{
// Closed gracefully.
_http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
CancelRequestAbortedToken();
}

void IHttpOutputAborter.OnInputOrOutputCompleted()
{
_http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
CancelRequestAbortedToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 226,7 @@ protected void ThrowUnexpectedEndOfRequestContent()
// so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400
// response is written after observing the unexpected end of request content instead of just
// closing the connection without a response as expected.
_context.OnInputOrOutputCompleted();
((IHttpOutputAborter)_context).OnInputOrOutputCompleted();

KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 161,8 @@ public Http2Connection(HttpConnectionContext context)
public void OnInputOrOutputCompleted()
{
TryClose();
_frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0;
_frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
}

public void Abort(ConnectionAbortedException ex)
Expand Down
10 changes: 10 additions & 0 deletions src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 27,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core;
public class KestrelServerOptions
{
internal const string DisableHttp1LineFeedTerminatorsSwitchKey = "Microsoft.AspNetCore.Server.Kestrel.DisableHttp1LineFeedTerminators";
private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
private static readonly bool _finOnError;

static KestrelServerOptions()
{
AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
}

// internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
internal static readonly Func<string, Encoding?> DefaultHeaderEncodingSelector = _ => null;

// Opt-out flag for back compat. Remove in 9.0 (or make public).
internal bool FinOnError { get; set; } = _finOnError;

private Func<string, Encoding?> _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector;

private Func<string, Encoding?> _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 29,7 @@ internal sealed partial class SocketConnection : TransportConnection
private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource();
private bool _connectionClosed;
private readonly bool _waitForData;
private readonly bool _finOnError;

internal SocketConnection(Socket socket,
MemoryPool<byte> memoryPool,
Expand All @@ -37,7 38,8 @@ internal SocketConnection(Socket socket,
SocketSenderPool socketSenderPool,
PipeOptions inputOptions,
PipeOptions outputOptions,
bool waitForData = true)
bool waitForData = true,
bool finOnError = false)
{
Debug.Assert(socket != null);
Debug.Assert(memoryPool != null);
Expand All @@ -48,6 50,7 @@ internal SocketConnection(Socket socket,
_logger = logger;
_waitForData = waitForData;
_socketSenderPool = socketSenderPool;
_finOnError = finOnError;

LocalEndPoint = _socket.LocalEndPoint;
RemoteEndPoint = _socket.RemoteEndPoint;
Expand Down Expand Up @@ -376,11 379,21 @@ private void Shutdown(Exception? shutdownReason)
// ever observe this ConnectionAbortedException except for connection middleware attempting
// to half close the connection which is currently unsupported. The message is always logged though.
_shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully.");

// NB: not _shutdownReason since we don't want to do this on graceful completion
if (!_finOnError && shutdownReason is not null)
{
SocketsLog.ConnectionWriteRst(_logger, this, shutdownReason.Message);

// This forces an abortive close with linger time 0 (and implies Dispose)
_socket.Close(timeout: 0);
return;
}

SocketsLog.ConnectionWriteFin(_logger, this, _shutdownReason.Message);

try
{
// Try to gracefully close the socket even for aborts to match libuv behavior.
_socket.Shutdown(SocketShutdown.Both);
}
catch
Expand Down
11 changes: 11 additions & 0 deletions src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 31,17 @@ public static void ConnectionWriteFin(ILogger logger, SocketConnection connectio
}
}

[LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = "ConnectionWriteRst", SkipEnabledCheck = true)]
private static partial void ConnectionWriteRstCore(ILogger logger, string connectionId, string reason);

public static void ConnectionWriteRst(ILogger logger, SocketConnection connection, string reason)
{
if (logger.IsEnabled(LogLevel.Debug))
{
ConnectionWriteRstCore(logger, connection.ConnectionId, reason);
}
}

// Reserved: Event ID 11, EventName = ConnectionWrite

// Reserved: Event ID 12, EventName = ConnectionWriteCallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 96,8 @@ public ConnectionContext Create(Socket socket)
setting.SocketSenderPool,
setting.InputOptions,
setting.OutputOptions,
waitForData: _options.WaitForDataBeforeAllocatingBuffer);
waitForData: _options.WaitForDataBeforeAllocatingBuffer,
finOnError: _options.FinOnError);

connection.Start();
return connection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 23,12 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions)
MaxWriteBufferSize = transportOptions.MaxWriteBufferSize;
UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling;
MemoryPoolFactory = transportOptions.MemoryPoolFactory;
FinOnError = transportOptions.FinOnError;
}

// Opt-out flag for back compat. Remove in 9.0 (or make public).
internal bool FinOnError { get; set; }

/// <summary>
/// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 13,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
/// </summary>
public class SocketTransportOptions
{
private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
private static readonly bool _finOnError;

static SocketTransportOptions()
{
AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
}

// Opt-out flag for back compat. Remove in 9.0 (or make public).
internal bool FinOnError { get; set; } = _finOnError;

/// <summary>
/// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 138,7 @@ public async Task ReceiveEnd(params string[] lines)
public async Task WaitForConnectionClose()
{
var buffer = new byte[128];
var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout);
var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).ContinueWith(t => t.IsFaulted ? 0 : t.Result).TimeoutAfter(Timeout);

if (bytesTransferred > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 48,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions
{
}

public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions)
public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions, Action<IServiceCollection> configureServices = null)
: this(app, context, options =>
{
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
Expand All @@ -57,7 57,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action<Listen
};
configureListenOptions(listenOptions);
options.CodeBackedListenOptions.Add(listenOptions);
}, _ => { })
}, s =>
{
configureServices?.Invoke(s);
})
{
}

Expand Down
54 changes: 54 additions & 0 deletions src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 44,60 @@ public ShutdownTests()
};
}

[ConditionalFact]
public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN()
{
var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

TestSink.MessageLogged = context =>
{
if (context.EventId.Name == "Http2ConnectionClosed")
{
connectionClosed.SetResult();
}
else if (context.EventId.Name == "ConnectionReadFin")
{
readFin.SetResult();
}
else if (context.EventId.Name == "ConnectionWriteFin")
{
writeFin.SetResult();
}
};

var testContext = new TestServiceContext(LoggerFactory);

testContext.InitializeHeartbeat();

await using (var server = new TestServer(context =>
{
return context.Response.WriteAsync("hello world " context.Request.Protocol);
},
testContext,
kestrelOptions =>
{
kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
listenOptions.UseHttps(_x509Certificate2);
});
}))
{
var response = await Client.GetStringAsync($"https://localhost:{server.Port}/");
Assert.Equal("hello world HTTP/2", response);
Client.Dispose(); // Close the socket, no GoAway is sent.

await readFin.Task.DefaultTimeout();
await writeFin.Task.DefaultTimeout();
await connectionClosed.Task.DefaultTimeout();

await server.StopAsync();
}
}

[CollectDump]
[ConditionalFact]
public async Task GracefulShutdownWaitsForRequestsToFinish()
Expand Down
Loading

0 comments on commit acfe811

Please sign in to comment.