Skip to content

Downgrade to HTTP/1.1 on older Windows versions #22859

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 12 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -563,12 +563,9 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="RequestTrailersNotAvailable" xml:space="preserve">
<value>The request trailers are not available yet. They may not be available until the full request body is read.</value>
</data>
<data name="HTTP2NoTlsOsx" xml:space="preserve">
<data name="Http2NoTlsOsx" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on macOS due to missing ALPN support.</value>
</data>
<data name="HTTP2NoTlsWin7" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on Windows 7 due to missing ALPN support.</value>
</data>
<data name="Http2StreamResetByApplication" xml:space="preserve">
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
</data>
Expand Down Expand Up @@ -605,6 +602,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="HttpsConnectionEstablished" xml:space="preserve">
<value>Connection "{connectionId}" established using the following protocol: {protocol}</value>
</data>
<data name="Http2DefaultCiphersInsufficient" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on Windows versions older than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support. Falling back to HTTP/1.1 instead.</value>
Copy link
Member

@halter73 halter73 Jun 16, 2020

Choose a reason for hiding this comment

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

Should we mention how to change the enabled ciphers and re-enable HTTP/2 on older Windows versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the best we can do is get a aka.ms link to something like https://docs.microsoft.com/en-us/windows/win32/secauthn/tls-cipher-suites-in-windows-8-1, would that be sufficient?

Copy link
Member

Choose a reason for hiding this comment

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

A breaking change announcement should be sufficient. We can follow up with anyone that's actually trying to make it work.

</data>
<data name="Http2NoTlsWin81" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on Windows versions earlier than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support.</value>
</data>
<data name="Http2ErrorKeepAliveTimeout" xml:space="preserve">
<value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
internal class HttpsConnectionMiddleware
{
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
private readonly ConnectionDelegate _next;
private readonly HttpsConnectionAdapterOptions _options;
private readonly ILogger _logger;
Expand All @@ -43,18 +44,26 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
throw new ArgumentNullException(nameof(options));
}

_options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();

// This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
if (options.HttpProtocols == HttpProtocols.Http2)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
throw new NotSupportedException(CoreStrings.HTTP2NoTlsOsx);
throw new NotSupportedException(CoreStrings.Http2NoTlsOsx);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version < new Version(6, 2))
Copy link
Member

Choose a reason for hiding this comment

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

Is there any risk that someone got ALPN working on Windows 8 somehow and this change breaks them despite the AppContext switch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This probably warrants a breaking changes announcement.

Copy link
Member

Choose a reason for hiding this comment

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

This definitely warrants a breaking change announcement.

If someone does have ALPN working on Windows 8, changing < new Version(6, 2) to < new Version(6, 3) would completely break them and not even the AppContext switch would work around the issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ALPN support was added in Windows 8.1 so I don't think that was ever a scenario that worked. I made the change since I noticed that Windows 8.1 which was where ALPN support was first added is version 6.3 instead of 6.2. Unless there was an actual reason for allowing Http2 on Windows 8?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ping @Tratcher

Copy link
Member

Choose a reason for hiding this comment

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

No reason to allow it on Win8 without ALPN, it's the same as Win7.

else if (IsWindowsVersionIncompatible())
{
throw new NotSupportedException(CoreStrings.HTTP2NoTlsWin7);
throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
}
}
else if (options.HttpProtocols == HttpProtocols.Http1AndHttp2 && IsWindowsVersionIncompatible())
{
_logger.Http2DefaultCiphersInsufficient();
options.HttpProtocols = HttpProtocols.Http1;
}

_next = next;
// capture the certificate now so it can't be switched after validation
Expand All @@ -75,9 +84,6 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
{
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
}

_options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
}

public async Task OnConnectionAsync(ConnectionContext context)
Expand Down Expand Up @@ -214,7 +220,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);

_logger.LogDebug(2, CoreStrings.AuthenticationTimedOut);
_logger.AuthenticationTimedOut();
await sslStream.DisposeAsync();
return;
}
Expand All @@ -223,7 +229,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);

_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
_logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync();
return;
}
Expand All @@ -232,7 +238,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);

_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
_logger.AuthenticationFailed(ex);

await sslStream.DisposeAsync();
return;
Expand All @@ -252,7 +258,7 @@ public async Task OnConnectionAsync(ConnectionContext context)

KestrelEventSource.Log.TlsHandshakeStop(context, feature);

_logger.LogDebug(3, CoreStrings.HttpsConnectionEstablished, context.ConnectionId, sslStream.SslProtocol);
_logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);

var originalTransport = context.Transport;

Expand Down Expand Up @@ -298,5 +304,57 @@ private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certif

return new X509Certificate2(certificate);
}

private static bool IsWindowsVersionIncompatible()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var enableHttp2OnWindows81 = AppContext.TryGetSwitch(EnableWindows81Http2, out var enabled) && enabled;
if (Environment.OSVersion.Version < new Version(6, 3) // Missing ALPN support
// Win8.1 and 2012 R2 don't support the right cipher configuration by default.
|| (Environment.OSVersion.Version < new Version(10, 0) && !enableHttp2OnWindows81))
{
return true;
}
}

return false;
}
}

internal static class HttpsConnectionMiddlewareLoggerExtensions
{

private static readonly Action<ILogger, Exception> _authenticationFailed =
LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: new EventId(1, "AuthenticationFailed"),
formatString: CoreStrings.AuthenticationFailed);

private static readonly Action<ILogger, Exception> _authenticationTimedOut =
LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: new EventId(2, "AuthenticationTimedOut"),
formatString: CoreStrings.AuthenticationTimedOut);

private static readonly Action<ILogger, string, SslProtocols, Exception> _httpsConnectionEstablished =
LoggerMessage.Define<string, SslProtocols>(
logLevel: LogLevel.Debug,
eventId: new EventId(3, "HttpsConnectionEstablished"),
formatString: CoreStrings.HttpsConnectionEstablished);

private static readonly Action<ILogger, Exception> _http2DefaultCiphersInsufficient =
LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: new EventId(4, "Http2DefaultCiphersInsufficient"),
formatString: CoreStrings.Http2DefaultCiphersInsufficient);

public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);

public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);

public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);

public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void TlsAndHttp2NotSupportedOnWin7()
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
{
using (var server = new TestServer(context =>
Expand Down Expand Up @@ -112,7 +112,7 @@ public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2()
{
using (var server = new TestServer(context =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ public void ThrowsForCertificatesMissingServerEku(string testCertName)
[InlineData(HttpProtocols.Http1AndHttp2)]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols)
{
void ConfigureListenOptions(ListenOptions listenOptions)
Expand Down Expand Up @@ -623,6 +623,65 @@ void ConfigureListenOptions(ListenOptions listenOptions)
stream.NegotiatedApplicationProtocol);
}

[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);

Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols);
}

[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);

Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols);
}

[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void Http2ThrowsOnIncompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http2
};

Assert.Throws<NotSupportedException>(() => new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions));
}

[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public void Http2DoesNotThrowOnCompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http2
};

// Does not throw
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
}

private static async Task App(HttpContext httpContext)
{
var request = httpContext.Request;
Expand Down