From 823c4e6eb1fa87c344ff29e230061af466738b2e Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 5 Sep 2023 20:23:12 +0200 Subject: [PATCH 1/3] Document how to manipulate SNI with HttpClient --- docs/core/extensions/httpclient-sni.md | 63 ++++++++++++++++++++++++++ docs/fundamentals/toc.yml | 3 ++ 2 files changed, 66 insertions(+) create mode 100644 docs/core/extensions/httpclient-sni.md diff --git a/docs/core/extensions/httpclient-sni.md b/docs/core/extensions/httpclient-sni.md new file mode 100644 index 0000000000000..df52174a406a4 --- /dev/null +++ b/docs/core/extensions/httpclient-sni.md @@ -0,0 +1,63 @@ +--- +title: Customize SNI in HTTP requests +description: Learn How to control Server Name Indication TLS extension in HTTP requests. +author: rzikm +ms.author: radekzikmund +ms.date: 9/5/2023 +--- + +# Customize SNI in HTTP requests + +When negotiating an HTTPS connection, a TLS connection needs to be established first. As part of the TLS handshake, the client sends a domain name of the server it is connecting to in one of the TLS extensions. When hosting multiple (virtual) servers on the same machine, this feature of TLS protocol allows distinguishing which of these servers clients are connecting to and configure TLS settings, such as the server certificate, accordingly. + +When making a HTTP request using `HttpClient`, the implementation automatically selects value for the SNI extension based on the URL the client is connecting to. For scenarios which require more manual control of the extension, you can use one of the following ways. + +## Host header + +Host HTTP header performs similar function as the SNI extension in TLS. It lets target server distinguish among requests for multiple host names on a single IP address. `HttpClient` will automatically fill in the Host header using the request URI. However, you can also set its value manually, and `HttpClient` will also use the new value in the SNI extension. You can use either `HttpRequestMessage.Headers.Host` or `HttpClient.DefaultRequestHeaders.Host` to achieve this effect. + +```csharp +using HttpClient client = new(); + +client.DefaultRequestHeaders.Host = "www.microsoft.com"; + +using var response = await client.GetAsync("https://127.0.0.1:5001/"); + +System.Console.WriteLine(response); +``` + +> [!NOTE] +> This method will not allow you to avoid sending SNI altogether when connecting to a URL with a hostname. If the header is set to empty string, `HttpClient` will use the hostname from the URL instead. + +> [!NOTE] +> Customizing the Host header affects server certificate validation. By default, client will expect the server certificate to match the hostname in the Host header. + +## Manual SslStream authentication via ConnectCallback + +A more complicated, but more powerful option is to use the `SocketsHttpHandler.ConnectCallback`. Since .NET 7, it is possible to return authenticated `SslStream` and thus customise how the TLS connection is being established. Inside the callback, arbitrary `SslClientAuthenticationOptions` can be used to perform client-side authentication. + +```csharp +var handler = new SocketsHttpHandler +{ + ConnectCallback = async (context, cancellationToken) => + { + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken); + + var sslStream = new SslStream(new NetworkStream(socket)); + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = context.DnsEndPoint.Host, + + }, cancellationToken); + + return sslStream; + } +}; + +using HttpClient client = new(handler); + +using var response = await client.GetAsync("https://www.microsoft.com"); + +System.Console.WriteLine(response); +``` diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index 4220565bd2a65..ce1a30cf5c903 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -833,6 +833,9 @@ items: - name: Rate limit an HTTP handler href: ../core/extensions/http-ratelimiter.md displayName: networking,http,rate limit,rate limiting,rate limit http,rate limiting http,rate limit http handler,rate limiting http handler + - name: Customize SNI in HTTP requests + href: ../core/extensions/httpclient-sni.md + displayName: networking,http,httpclient,sni,server name indication,server name indication http,server name indication httpclient,server name indication http client - name: Sockets items: - name: Sockets support From 7937a9f0db9916eb56b7cb01598b705e67cf032f Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Wed, 6 Sep 2023 08:33:21 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- docs/core/extensions/httpclient-sni.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/core/extensions/httpclient-sni.md b/docs/core/extensions/httpclient-sni.md index df52174a406a4..cac12c2cd5c2c 100644 --- a/docs/core/extensions/httpclient-sni.md +++ b/docs/core/extensions/httpclient-sni.md @@ -8,13 +8,13 @@ ms.date: 9/5/2023 # Customize SNI in HTTP requests -When negotiating an HTTPS connection, a TLS connection needs to be established first. As part of the TLS handshake, the client sends a domain name of the server it is connecting to in one of the TLS extensions. When hosting multiple (virtual) servers on the same machine, this feature of TLS protocol allows distinguishing which of these servers clients are connecting to and configure TLS settings, such as the server certificate, accordingly. +When negotiating an HTTPS connection, a TLS connection needs to be established first. As part of the TLS handshake, the client sends the domain name of the server it's connecting to in one of the TLS extensions. When hosting multiple (virtual) servers on the same machine, this feature of the TLS protocol allows clients to distinguish which of these servers they're connecting to and to configure TLS settings, such as the server certificate, accordingly. -When making a HTTP request using `HttpClient`, the implementation automatically selects value for the SNI extension based on the URL the client is connecting to. For scenarios which require more manual control of the extension, you can use one of the following ways. +When making an HTTP request using `HttpClient`, the implementation automatically selects a value for the server name indication (SNI) extension based on the URL the client is connecting to. For scenarios that require more manual control of the extension, you can use one of the following approaches. ## Host header -Host HTTP header performs similar function as the SNI extension in TLS. It lets target server distinguish among requests for multiple host names on a single IP address. `HttpClient` will automatically fill in the Host header using the request URI. However, you can also set its value manually, and `HttpClient` will also use the new value in the SNI extension. You can use either `HttpRequestMessage.Headers.Host` or `HttpClient.DefaultRequestHeaders.Host` to achieve this effect. +Host HTTP header performs a similar function as the SNI extension in TLS. It lets the target server distinguish among requests for multiple host names on a single IP address. `HttpClient` automatically fills in the Host header using the request URI. However, you can also set its value manually, and `HttpClient` will also use the new value in the SNI extension. You can use either `HttpRequestMessage.Headers.Host` or `HttpClient.DefaultRequestHeaders.Host` to achieve this effect. ```csharp using HttpClient client = new(); @@ -27,14 +27,14 @@ System.Console.WriteLine(response); ``` > [!NOTE] -> This method will not allow you to avoid sending SNI altogether when connecting to a URL with a hostname. If the header is set to empty string, `HttpClient` will use the hostname from the URL instead. +> This method doesn't allow you to avoid sending SNI altogether when connecting to a URL with a hostname. If the header is set to empty string, `HttpClient` uses the hostname from the URL instead. > [!NOTE] > Customizing the Host header affects server certificate validation. By default, client will expect the server certificate to match the hostname in the Host header. ## Manual SslStream authentication via ConnectCallback -A more complicated, but more powerful option is to use the `SocketsHttpHandler.ConnectCallback`. Since .NET 7, it is possible to return authenticated `SslStream` and thus customise how the TLS connection is being established. Inside the callback, arbitrary `SslClientAuthenticationOptions` can be used to perform client-side authentication. +A more complicated, but also more powerful, option is to use the `SocketsHttpHandler.ConnectCallback`. Since .NET 7, it is possible to return an authenticated `SslStream` and thus customize how the TLS connection is established. Inside the callback, arbitrary `SslClientAuthenticationOptions` options can be used to perform client-side authentication. ```csharp var handler = new SocketsHttpHandler From 67275d45641a9b2c9e38f1b25b23ded18dfaeac3 Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:16:47 +0200 Subject: [PATCH 3/3] Update docs/core/extensions/httpclient-sni.md Co-authored-by: Miha Zupan --- docs/core/extensions/httpclient-sni.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/core/extensions/httpclient-sni.md b/docs/core/extensions/httpclient-sni.md index cac12c2cd5c2c..e1a9379993c3f 100644 --- a/docs/core/extensions/httpclient-sni.md +++ b/docs/core/extensions/httpclient-sni.md @@ -41,17 +41,27 @@ var handler = new SocketsHttpHandler { ConnectCallback = async (context, cancellationToken) => { - var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); - await socket.ConnectAsync(context.DnsEndPoint, cancellationToken); - - var sslStream = new SslStream(new NetworkStream(socket)); - await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try { - TargetHost = context.DnsEndPoint.Host, + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken); + + var sslStream = new SslStream(new NetworkStream(socket, ownsSocket: true)); - }, cancellationToken); + // When using HTTP/2, you must also keep in mind to set options like ApplicationProtocols + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = context.DnsEndPoint.Host, - return sslStream; + }, cancellationToken); + + return sslStream; + } + catch + { + socket.Dispose(); + throw; + } } };