Skip to content

Commit ecbc54d

Browse files
committed
support tls client hello bytes callback in Kestrel
1 parent eedcac3 commit ecbc54d

File tree

6 files changed

+720
-0
lines changed

6 files changed

+720
-0
lines changed

src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Net.Security;
56
using System.Security.Authentication;
67
using System.Security.Cryptography.X509Certificates;
@@ -96,6 +97,12 @@ public void AllowAnyClientCertificate()
9697
/// </summary>
9798
public Action<ConnectionContext, SslServerAuthenticationOptions>? OnAuthenticate { get; set; }
9899

100+
/// <summary>
101+
/// A callback to be invoked to get the TLS client hello bytes.
102+
/// Null by default.
103+
/// </summary>
104+
public Action<ConnectionContext, ReadOnlySequence<byte>>? TlsClientHelloBytesCallback { get; set; }
105+
99106
/// <summary>
100107
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive
101108
/// or <see cref="Timeout.InfiniteTimeSpan"/>. Defaults to 10 seconds.

src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Security.Cryptography.X509Certificates;
66
using Microsoft.AspNetCore.Server.Kestrel.Core;
77
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
8+
using Microsoft.AspNetCore.Server.Kestrel.Core.Middleware;
89
using Microsoft.AspNetCore.Server.Kestrel.Https;
910
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
1011
using Microsoft.Extensions.DependencyInjection;
@@ -197,6 +198,15 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConn
197198
listenOptions.IsTls = true;
198199
listenOptions.HttpsOptions = httpsOptions;
199200

201+
if (httpsOptions.TlsClientHelloBytesCallback is not null)
202+
{
203+
listenOptions.Use(next =>
204+
{
205+
var middleware = new TlsListenerMiddleware(next, httpsOptions.TlsClientHelloBytesCallback);
206+
return middleware.OnTlsClientHelloAsync;
207+
});
208+
}
209+
200210
listenOptions.Use(next =>
201211
{
202212
var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics);
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
using Microsoft.AspNetCore.Connections;
6+
7+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Middleware;
8+
9+
internal sealed class TlsListenerMiddleware
10+
{
11+
private readonly ConnectionDelegate _next;
12+
private readonly Action<ConnectionContext, ReadOnlySequence<byte>> _tlsClientHelloBytesCallback;
13+
14+
public TlsListenerMiddleware(ConnectionDelegate next, Action<ConnectionContext, ReadOnlySequence<byte>> tlsClientHelloBytesCallback)
15+
{
16+
_next = next;
17+
_tlsClientHelloBytesCallback = tlsClientHelloBytesCallback;
18+
}
19+
20+
/// <summary>
21+
/// Sniffs the TLS Client Hello message, and invokes a callback if found.
22+
/// </summary>
23+
internal async Task OnTlsClientHelloAsync(ConnectionContext connection)
24+
{
25+
var input = connection.Transport.Input;
26+
27+
while (true)
28+
{
29+
var result = await input.ReadAsync();
30+
var buffer = result.Buffer;
31+
32+
// If the buffer length is less than 6 bytes (handshake + version + length + client-hello byte)
33+
// and no more data is coming, we can't block in a loop here because we will not get more data
34+
if (buffer.Length < 6 && result.IsCompleted)
35+
{
36+
break;
37+
}
38+
39+
var parseState = TryParseClientHello(buffer, out var clientHelloBytes);
40+
41+
// no data is consumed, it will be processed by the follow-up middlewares
42+
input.AdvanceTo(buffer.Start);
43+
44+
switch (parseState)
45+
{
46+
case ClientHelloParseState.NotEnoughData:
47+
continue;
48+
49+
case ClientHelloParseState.NotTlsClientHello:
50+
await _next(connection);
51+
return;
52+
53+
case ClientHelloParseState.ValidTlsClientHello:
54+
_tlsClientHelloBytesCallback(connection, clientHelloBytes);
55+
await _next(connection);
56+
return;
57+
}
58+
}
59+
60+
await _next(connection);
61+
}
62+
63+
private static ClientHelloParseState TryParseClientHello(ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> clientHelloBytes)
64+
{
65+
clientHelloBytes = default;
66+
67+
if (buffer.Length < 6)
68+
{
69+
return ClientHelloParseState.NotEnoughData;
70+
}
71+
72+
var reader = new SequenceReader<byte>(buffer);
73+
74+
// Content type must be 0x16 for TLS Handshake
75+
if (!reader.TryRead(out byte contentType) || contentType != 0x16)
76+
{
77+
return ClientHelloParseState.NotTlsClientHello;
78+
}
79+
80+
// Protocol version
81+
if (!reader.TryReadBigEndian(out short version) || IsValidProtocolVersion(version) == false)
82+
{
83+
return ClientHelloParseState.NotTlsClientHello;
84+
}
85+
86+
// Record length
87+
if (!reader.TryReadBigEndian(out short recordLength))
88+
{
89+
return ClientHelloParseState.NotTlsClientHello;
90+
}
91+
92+
// 5 bytes are
93+
// 1) Handshake (1 byte)
94+
// 2) Protocol version (2 bytes)
95+
// 3) Record length (2 bytes)
96+
if (buffer.Length < 5 + recordLength)
97+
{
98+
return ClientHelloParseState.NotEnoughData;
99+
}
100+
101+
// byte 6: handshake message type (must be 0x01 for ClientHello)
102+
if (!reader.TryRead(out byte handshakeType) || handshakeType != 0x01)
103+
{
104+
return ClientHelloParseState.NotTlsClientHello;
105+
}
106+
107+
clientHelloBytes = buffer.Slice(0, 5 + recordLength);
108+
return ClientHelloParseState.ValidTlsClientHello;
109+
}
110+
111+
private static bool IsValidProtocolVersion(short version)
112+
=> version == 0x0002 // SSL 2.0 (0x0002)
113+
|| version == 0x0300 // SSL 3.0 (0x0300)
114+
|| version == 0x0301 // TLS 1.0 (0x0301)
115+
|| version == 0x0302 // TLS 1.1 (0x0302)
116+
|| version == 0x0303 // TLS 1.2 (0x0303)
117+
|| version == 0x0304; // TLS 1.3 (0x0304)
118+
119+
private enum ClientHelloParseState
120+
{
121+
NotEnoughData,
122+
NotTlsClientHello,
123+
ValidTlsClientHello
124+
}
125+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.TlsClientHelloBytesCallback.get -> System.Action<Microsoft.AspNetCore.Connections.ConnectionContext!, System.Buffers.ReadOnlySequence<byte>>?
3+
Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.TlsClientHelloBytesCallback.set -> void

0 commit comments

Comments
 (0)