File: DirectTlsTransportFactory.cs
Web Access
Project: src\aspnetcore\src\Servers\Kestrel\Transport.DirectTls\src\Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.csproj (Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.Connection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls;
 
/// <summary>
/// A factory for DirectTls (native, fd-bound OpenSSL) connections. Binds only <see cref="DirectTlsEndpoint"/>
/// endpoints; every other endpoint type is left to the default transport.
/// </summary>
internal sealed class DirectTlsTransportFactory : IConnectionListenerFactory, IConnectionListenerFactorySelector
{
    private readonly DirectTlsTransportOptions _options;
 
    private readonly ILoggerFactory _loggerFactory;
    private readonly ILogger _logger;
    private readonly IHostApplicationLifetime _applicationLifetime;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="DirectTlsTransportFactory"/> class.
    /// </summary>
    /// <param name="options">The transport options.</param>
    /// <param name="loggerFactory">The logger factory.</param>
    /// <param name="applicationLifetime">
    /// The host application lifetime, used to stop the host if a pump fails unrecoverably. Supplied by the DI
    /// container.
    /// </param>
    public DirectTlsTransportFactory(
        IOptions<DirectTlsTransportOptions> options,
        ILoggerFactory loggerFactory,
        IHostApplicationLifetime applicationLifetime)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(loggerFactory);
        ArgumentNullException.ThrowIfNull(applicationLifetime);
 
        _options = options.Value;
        _loggerFactory = loggerFactory;
        _logger = loggerFactory.CreateLogger<DirectTlsTransportFactory>();
        _applicationLifetime = applicationLifetime;
    }
 
    /// <inheritdoc />
    public async ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
    {
        if (!OperatingSystem.IsLinux())
        {
            throw new PlatformNotSupportedException("The DirectTls transport requires a Linux operating system.");
        }
 
        if (endpoint is not DirectTlsEndpoint directTlsEndpoint)
        {
            throw new NotSupportedException(
                $"The DirectTls transport only binds {nameof(DirectTlsEndpoint)} endpoints.");
        }
 
        var endpointOptions = directTlsEndpoint.Options;
 
        // DirectTls is TLS-only: a server certificate is mandatory.
        if (endpointOptions.ServerCertificate is null && endpointOptions.ServerCertificateSelector is null)
        {
            throw new InvalidOperationException(
                $"A server certificate is required for a {nameof(DirectTlsEndpoint)}. Set " +
                $"{nameof(DirectTlsEndpointOptions.ServerCertificate)} or " +
                $"{nameof(DirectTlsEndpointOptions.ServerCertificateSelector)} on the endpoint options.");
        }
 
        if (endpointOptions.ClientCertificateMode == ClientCertificateMode.DelayCertificate)
        {
            throw new NotSupportedException(
                $"{nameof(ClientCertificateMode)}.{nameof(ClientCertificateMode.DelayCertificate)} is not supported by the DirectTls transport.");
        }
 
        // ALPN list advertised during the handshake, derived from the endpoint's HTTP protocols (server
        // preference h2 first). This is what lets HttpConnection.SelectProtocol negotiate HTTP/2. The
        // protocols were copied from the endpoint's ListenOptions.Protocols by DirectTlsEndpointProtocolsSetup.
        var applicationProtocols = BuildApplicationProtocols(endpointOptions.HttpProtocols);
 
        bool requireClientCertificate =
            endpointOptions.ClientCertificateMode is ClientCertificateMode.AllowCertificate or ClientCertificateMode.RequireCertificate;
 
        // Non-null only when the endpoint requests a client certificate. The pump invokes this at handshake
        // completion (the fd fast path cannot surface the mTLS verdict itself) and drops rejected connections.
        RemoteCertificateValidationCallback? clientCertificateValidation =
            requireClientCertificate ? BuildClientCertificateValidation(endpointOptions) : null;
 
        // Bootstrap context created WITHOUT server credentials. On the runtime's socket-bound (fd) handshake
        // this forces the deferred model: the first Handshake() parses the ClientHello and returns
        // NeedsTlsContext, at which point the pump asks the resolver below for the real per-host context.
        TlsContext bootstrapContext = TlsContext.CreateServer(new SslServerAuthenticationOptions());
 
        // Per-certificate TlsContext cache so repeated SNI resolutions of the same certificate reuse one
        // native context (creating a TlsContext acquires OpenSSL credentials).
        var contextCache = new ConcurrentDictionary<X509Certificate2, TlsContext>();
 
        Func<ConnectionContext?, string?, (TlsContext Context, RemoteCertificateValidationCallback? ClientCertificateValidation)> contextResolver =
            (connection, hostName) =>
            {
                var certificate = endpointOptions.ServerCertificateSelector?.Invoke(connection, hostName)
                    ?? endpointOptions.ServerCertificate;
 
                if (certificate is null)
                {
                    throw new AuthenticationException(
                        $"No server certificate was resolved for SNI host name '{hostName}'.");
                }
 
                var context = contextCache.GetOrAdd(certificate, cert =>
                {
                    var authenticationOptions = new SslServerAuthenticationOptions
                    {
                        ServerCertificate = cert,
                        EnabledSslProtocols = endpointOptions.SslProtocols,
                        ApplicationProtocols = applicationProtocols,
                        ClientCertificateRequired = requireClientCertificate,
                    };
 
                    if (clientCertificateValidation is not null)
                    {
                        authenticationOptions.RemoteCertificateValidationCallback = clientCertificateValidation;
                    }
 
                    return TlsContext.CreateServer(authenticationOptions);
                });
 
                return (context, clientCertificateValidation);
            };
 
        Action<ConnectionContext, ReadOnlySequence<byte>>? clientHelloCallback = endpointOptions.TlsClientHelloBytesCallback;
 
        // Each listener owns its own pump pool bound to its own listen socket. This keeps endpoints fully
        // isolated so per-endpoint certificate selection (e.g. two ports with different certs) works
        // correctly, at the cost of WorkerCount threads per endpoint. The endpoint may override the
        // transport-wide worker count so multi-endpoint servers can bound their total thread count.
        var workerCount = endpointOptions.WorkerCount ?? _options.WorkerCount;
        var pumpPool = new TlsEventPumpPool(workerCount, _loggerFactory, endpointOptions.HandshakeTimeout);
 
        var memoryPool = _options.MemoryPoolFactory.Create(DirectTlsTransportOptions.MemoryPoolOptions);
 
        // The listener owns the native contexts (bootstrap + per-SNI cache) and disposes them on teardown.
        var ownedServerContexts = new ServerTlsContexts(bootstrapContext, contextCache);
 
        var transport = new DirectTlsConnectionListener(
            _loggerFactory,
            bootstrapContext,
            contextResolver,
            pumpPool,
            endpoint,
            _options,
            memoryPool,
            _applicationLifetime,
            clientHelloCallback,
            ownedServerContexts);
 
        _logger.LogInformation("DirectTls listener bound for endpoint {Endpoint}.", endpoint);
 
        try
        {
            transport.Bind();
        }
        catch
        {
            await transport.DisposeAsync().ConfigureAwait(false);
            throw;
        }
 
        return transport;
    }
 
    /// <inheritdoc />
    public bool CanBind(EndPoint endpoint)
    {
        // DirectTls binds ONLY endpoints explicitly opted in via DirectTlsEndpoint. Every other endpoint
        // (including plain IPEndPoint) falls through to the default transport.
        return endpoint is DirectTlsEndpoint;
    }
 
    // Builds the ALPN protocol list from the endpoint's HttpProtocols. Server preference is h2 first, then
    // http/1.1, matching the order the SslStream-based HTTPS path offers.
    private static List<SslApplicationProtocol> BuildApplicationProtocols(HttpProtocols protocols)
    {
        var applicationProtocols = new List<SslApplicationProtocol>();
 
        if ((protocols & HttpProtocols.Http2) == HttpProtocols.Http2)
        {
            applicationProtocols.Add(SslApplicationProtocol.Http2);
        }
 
        if ((protocols & HttpProtocols.Http1) == HttpProtocols.Http1)
        {
            applicationProtocols.Add(SslApplicationProtocol.Http11);
        }
 
        return applicationProtocols;
    }
 
    // Maps the endpoint's ClientCertificateMode + optional user validation callback onto the
    // RemoteCertificateValidationCallback the pump runs at handshake completion. Only built for
    // Allow/Require modes.
    internal static RemoteCertificateValidationCallback BuildClientCertificateValidation(DirectTlsEndpointOptions endpointOptions)
    {
        var clientCertificateMode = endpointOptions.ClientCertificateMode;
        var userValidation = endpointOptions.ClientCertificateValidation;
 
        bool ValidateCertificate(X509Certificate2 certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
        {
            return userValidation is not null
                ? userValidation(certificate, chain, sslPolicyErrors)
                : sslPolicyErrors == SslPolicyErrors.None;
        }
 
        return (sender, certificate, chain, sslPolicyErrors) =>
        {
            if (certificate is null)
            {
                // AllowCertificate tolerates a missing client cert; RequireCertificate rejects it.
                return clientCertificateMode != ClientCertificateMode.RequireCertificate;
            }
 
            if (certificate is X509Certificate2 certificate2)
            {
                return ValidateCertificate(certificate2, chain, sslPolicyErrors);
            }
 
            using var convertedCertificate = X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
 
            return ValidateCertificate(convertedCertificate, chain, sslPolicyErrors);
        };
    }
}