Skip to content

Commit 0862db9

Browse files
authored
feat: support tls client hello bytes callback in Kestrel (#61631)
1 parent 21ba24a commit 0862db9

File tree

7 files changed

+864
-0
lines changed

7 files changed

+864
-0
lines changed

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

Lines changed: 9 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,14 @@ 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+
/// If you want to store the bytes from the <see cref="System.Buffers.ReadOnlySequence{T}"/>,
104+
/// copy them into a buffer that you control rather than keeping a reference to the <see cref="ReadOnlySequence{T}"/> or <see cref="ReadOnlyMemory{T}"/> instances.
105+
/// </summary>
106+
public Action<ConnectionContext, ReadOnlySequence<byte>>? TlsClientHelloBytesCallback { get; set; }
107+
99108
/// <summary>
100109
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive
101110
/// 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: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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 System.Diagnostics;
6+
using Microsoft.AspNetCore.Connections;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Middleware;
9+
10+
internal sealed class TlsListenerMiddleware
11+
{
12+
private readonly ConnectionDelegate _next;
13+
private readonly Action<ConnectionContext, ReadOnlySequence<byte>> _tlsClientHelloBytesCallback;
14+
15+
public TlsListenerMiddleware(ConnectionDelegate next, Action<ConnectionContext, ReadOnlySequence<byte>> tlsClientHelloBytesCallback)
16+
{
17+
_next = next;
18+
_tlsClientHelloBytesCallback = tlsClientHelloBytesCallback;
19+
}
20+
21+
/// <summary>
22+
/// Sniffs the TLS Client Hello message, and invokes a callback if found.
23+
/// </summary>
24+
internal async Task OnTlsClientHelloAsync(ConnectionContext connection)
25+
{
26+
var input = connection.Transport.Input;
27+
ClientHelloParseState parseState = ClientHelloParseState.NotEnoughData;
28+
29+
while (true)
30+
{
31+
var result = await input.ReadAsync();
32+
var buffer = result.Buffer;
33+
34+
try
35+
{
36+
// If the buffer length is less than 6 bytes (handshake + version + length + client-hello byte)
37+
// and no more data is coming, we can't block in a loop here because we will not get more data
38+
if (result.IsCompleted && buffer.Length < 6)
39+
{
40+
break;
41+
}
42+
43+
parseState = TryParseClientHello(buffer, out var clientHelloBytes);
44+
if (parseState == ClientHelloParseState.NotEnoughData)
45+
{
46+
// if no data will be added, and we still lack enough bytes
47+
// we can't block in a loop, so just exit
48+
if (result.IsCompleted)
49+
{
50+
break;
51+
}
52+
53+
continue;
54+
}
55+
56+
if (parseState == ClientHelloParseState.ValidTlsClientHello)
57+
{
58+
_tlsClientHelloBytesCallback(connection, clientHelloBytes);
59+
}
60+
61+
Debug.Assert(parseState is ClientHelloParseState.ValidTlsClientHello or ClientHelloParseState.NotTlsClientHello);
62+
break; // We can continue with the middleware pipeline
63+
}
64+
finally
65+
{
66+
if (parseState is ClientHelloParseState.NotEnoughData)
67+
{
68+
input.AdvanceTo(buffer.Start, buffer.End);
69+
}
70+
else
71+
{
72+
// ready to continue middleware pipeline, reset the buffer to initial state
73+
input.AdvanceTo(buffer.Start);
74+
}
75+
}
76+
}
77+
78+
await _next(connection);
79+
}
80+
81+
/// <summary>
82+
/// RFCs
83+
/// ----
84+
/// TLS 1.1: https://datatracker.ietf.org/doc/html/rfc4346#section-6.2
85+
/// TLS 1.2: https://datatracker.ietf.org/doc/html/rfc5246#section-6.2
86+
/// TLS 1.3: https://datatracker.ietf.org/doc/html/rfc8446#section-5.1
87+
/// </summary>
88+
private static ClientHelloParseState TryParseClientHello(ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> clientHelloBytes)
89+
{
90+
clientHelloBytes = default;
91+
92+
if (buffer.Length < 6)
93+
{
94+
return ClientHelloParseState.NotEnoughData;
95+
}
96+
97+
var reader = new SequenceReader<byte>(buffer);
98+
99+
// Content type must be 0x16 for TLS Handshake
100+
if (!reader.TryRead(out byte contentType) || contentType != 0x16)
101+
{
102+
return ClientHelloParseState.NotTlsClientHello;
103+
}
104+
105+
// Protocol version
106+
if (!reader.TryReadBigEndian(out short version) || !IsValidProtocolVersion(version))
107+
{
108+
return ClientHelloParseState.NotTlsClientHello;
109+
}
110+
111+
// Record length
112+
if (!reader.TryReadBigEndian(out short recordLength))
113+
{
114+
return ClientHelloParseState.NotTlsClientHello;
115+
}
116+
117+
// byte 6: handshake message type (must be 0x01 for ClientHello)
118+
if (!reader.TryRead(out byte handshakeType) || handshakeType != 0x01)
119+
{
120+
return ClientHelloParseState.NotTlsClientHello;
121+
}
122+
123+
// 5 bytes are
124+
// 1) Handshake (1 byte)
125+
// 2) Protocol version (2 bytes)
126+
// 3) Record length (2 bytes)
127+
if (buffer.Length < 5 + recordLength)
128+
{
129+
return ClientHelloParseState.NotEnoughData;
130+
}
131+
132+
clientHelloBytes = buffer.Slice(0, 5 + recordLength);
133+
return ClientHelloParseState.ValidTlsClientHello;
134+
}
135+
136+
private static bool IsValidProtocolVersion(short version)
137+
=> version is 0x0300 // SSL 3.0 (0x0300)
138+
or 0x0301 // TLS 1.0 (0x0301)
139+
or 0x0302 // TLS 1.1 (0x0302)
140+
or 0x0303 // TLS 1.2 (0x0303)
141+
or 0x0304; // TLS 1.3 (0x0304)
142+
143+
private enum ClientHelloParseState : byte
144+
{
145+
NotEnoughData,
146+
NotTlsClientHello,
147+
ValidTlsClientHello
148+
}
149+
}
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
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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;
5+
using System.Collections.Generic;
6+
using System.IO.Pipelines;
7+
using System.Text;
8+
9+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests.TestHelpers;
10+
11+
internal class ObservablePipeReader : PipeReader
12+
{
13+
private readonly PipeReader _inner;
14+
15+
public ObservablePipeReader(PipeReader reader)
16+
{
17+
_inner = reader;
18+
}
19+
20+
/// <summary>
21+
/// Number of times <see cref="ReadAsync(CancellationToken)"/> was called.
22+
/// </summary>
23+
public int ReadAsyncCounter { get; private set; }
24+
25+
public override void AdvanceTo(SequencePosition consumed)
26+
=> _inner.AdvanceTo(consumed);
27+
28+
public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
29+
=> _inner.AdvanceTo(consumed, examined);
30+
31+
public override void CancelPendingRead()
32+
=> _inner.CancelPendingRead();
33+
34+
public override void Complete(Exception exception = null)
35+
=> _inner.Complete(exception);
36+
37+
public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
38+
{
39+
ReadAsyncCounter++;
40+
return _inner.ReadAsync(cancellationToken);
41+
}
42+
43+
public override bool TryRead(out ReadResult result)
44+
{
45+
return _inner.TryRead(out result);
46+
}
47+
}

0 commit comments

Comments
 (0)